Effective Python --編寫(xiě)高質(zhì)量Python代碼的59個(gè)有效方法 (讀書(shū)筆記)

Effective Python --編寫(xiě)高質(zhì)量Python代碼的59個(gè)有效方法 (讀書(shū)筆記 每日更新一條 Updated 2019.1.28)

好記性不如爛筆頭,讀讀寫(xiě)寫(xiě)才能記得牢固些

第 1 章 用 Pythonic 方式來(lái)思考 1

第 1 條:確認(rèn)自己所用的 Python 版本 1

2to3和six等工具可以吧Python2代碼適配到Python3版本上。

  • 有兩個(gè)版本的 Python 處于活躍狀態(tài),它們是: Python 2 與 Python 3。
  • 有很多種流行的 Python 運(yùn)行時(shí)環(huán)境,例如, CPython、 Jython、 IronPython 以及
    PyPy 等。
  • 在操作系統(tǒng)的命令行中運(yùn)行 Python 時(shí),請(qǐng)確保該 Python 的版本與你想使用的
    Python 版本相符。
  • 由于 Python 社區(qū)把開(kāi)發(fā)重點(diǎn)放在 Python 3 上,所以在開(kāi)發(fā)后續(xù)項(xiàng)目時(shí),應(yīng)該優(yōu)先考慮采用 Python 3。

第 2 條:遵循 PEP 8 風(fēng)格指南 3

PEP 8

  • 空白:

    • 不要使用 tab 縮進(jìn),使用空格來(lái)縮進(jìn)
    • 使用四個(gè)空格縮進(jìn),使用四個(gè)空格對(duì)長(zhǎng)表達(dá)式換行縮進(jìn)
    • 每行的字符數(shù)不應(yīng)該超過(guò) 79
    • class和funciton之間用兩個(gè)空行,class的method之間用一個(gè)空行
    • list索引和函數(shù)調(diào)用,關(guān)鍵字參數(shù)賦值不要在兩旁加空格
    • 變量賦值前后都用一個(gè)空格
  • 命名

    • 函數(shù),變量以及屬性應(yīng)該使用小寫(xiě),如果有多個(gè)單詞推薦使用下劃線(xiàn)進(jìn)行連接,如lowercase_underscore
    • 被保護(hù) 的屬性應(yīng)該使用 單個(gè) 前導(dǎo)下劃線(xiàn)來(lái)聲明。
    • 私有 的屬性應(yīng)該使用 兩個(gè) 前導(dǎo)下劃線(xiàn)來(lái)進(jìn)行聲明。
    • 類(lèi)以及異常信息 應(yīng)該使用單詞 首字母大寫(xiě) 形式,也就是我們經(jīng)常使用的駝峰命名法,如CapitalizedWord。
    • 模塊級(jí) 別的常量應(yīng)該使用 全部大寫(xiě) 的形式, 如ALL_CAPS。
    • 類(lèi)內(nèi)部的實(shí)例方法的應(yīng)該將self作為其第一個(gè)參數(shù)。且self也是對(duì)當(dāng)前類(lèi)對(duì)象的引用。
    • 類(lèi)方法應(yīng)該使用cls來(lái)作為其第一個(gè)參數(shù)。且self引用自當(dāng)前類(lèi)。
  • 表達(dá)式和語(yǔ)句( Python之禪: 每件事都應(yīng)該有直白的做法,而且最好只有一種 )

    • 使用內(nèi)聯(lián)否定(如 if a is not b) 而不是顯示的表達(dá)式(如if not a is b)。
    • 不要簡(jiǎn)單地通過(guò)變量的長(zhǎng)度(if len(somelist) == 0)來(lái)判斷空值。使用隱式的方式如來(lái)假設(shè)空值的情況(如if not somelist 與 False來(lái)進(jìn)行比較)。
    • 上面的第二條也適用于非空值(如[1],或者’hi’)。對(duì)這些非空值而言 if somelist默認(rèn)包含隱式的True。
    • 避免將if , for, while, except等包含多個(gè)語(yǔ)塊的表達(dá)式寫(xiě)在一行內(nèi),應(yīng)該分割成多行。
    • 總是把import語(yǔ)句寫(xiě)在Python文件的頂部。
    • 當(dāng)引用一個(gè)模塊的時(shí)候使用絕對(duì)的模塊名稱(chēng),而不是與當(dāng)前模塊路徑相關(guān)的名稱(chēng)。例如要想引入bar包下面的foo模塊,應(yīng)該使用from bar import foo而不是import foo。
    • 如果非要相對(duì)的引用,應(yīng)該使用明確的語(yǔ)法from . import foo。
    • 按照以下規(guī)則引入模塊:標(biāo)準(zhǔn)庫(kù),第三方庫(kù),你自己的庫(kù)。每一個(gè)部分內(nèi)部也應(yīng)該按照字母順序來(lái)引入。

第 3 條:了解 bytes、 str 與 unicode 的區(qū)別 5

  • Python3 兩種字符串類(lèi)型:bytes和str,bytes表示8-bit的二進(jìn)制值,str表示unicode字符。開(kāi)發(fā)者不能用 > 或 + 混同操作bytes和str實(shí)例
  • Python2 兩種字符串類(lèi)型:str和unicode,str表示8-bit的二進(jìn)制值,unicode表示unicode字符。如果str只包含7位ASCII字符,那么通過(guò)相關(guān)的操作符才同時(shí)使用str與unicode
  • 在對(duì)輸入的數(shù)據(jù)進(jìn)行操作之前,使用輔助函數(shù)來(lái)保證字符的序列
  • 從文件中讀取或者寫(xiě)入二進(jìn)制數(shù)據(jù)時(shí),總應(yīng)該使用 ‘rb’ 或 ‘wb’ 等二進(jìn)制模式來(lái)開(kāi)啟文件

第 4 條:用輔助函數(shù)來(lái)取代復(fù)雜的表達(dá)式 8

  • 開(kāi)發(fā)者很容易過(guò)度使用Python的語(yǔ)法特效,從而寫(xiě)出那種特別復(fù)雜并且難以理解的單行表達(dá)式
  • 請(qǐng)把復(fù)雜的表達(dá)式移入輔助函數(shù)中,如果要反復(fù)使用相同的邏輯,那就更應(yīng)該這么做
  • 使用 if/else 表達(dá)式,要比使用 or 或者 and 這樣的 Booolean 操作符更加清晰

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

  • 分片機(jī)制自動(dòng)處理越界問(wèn)題,但是最好在表達(dá)邊界大小范圍是更加的清晰。(如a[:20] 或者a[-20:])
  • list,str,bytes和實(shí)現(xiàn)__getitem____setitem__ 這兩個(gè)特殊方法的類(lèi)都支持slice操作
  • 基本形式是:somelist[start:end],不包括end,可以使用負(fù)數(shù),-1 表示最后一個(gè),默認(rèn)正向選取,下標(biāo)0可以省略,最后一個(gè)下標(biāo)也可以省略
  • slice list是shadow copy,somelist[0:]會(huì)復(fù)制原list,切割之后對(duì)新得到的列表進(jìn)行修改不會(huì)影響原來(lái)的列表
  • slice賦值會(huì)修改slice list,即使長(zhǎng)度不一致(增刪改)

第 6 條:在單次切片操作內(nèi),不要同時(shí)指定 start、 end 和 stride 13

  • 既有start和end, 又有stride的切割操作,會(huì)讓人感到困惑。
  • 盡量使用stride為正數(shù),且不帶start或者end索引的切割操作。盡可能的避免在分片中使用負(fù)數(shù)值。
  • 避免在分片中同時(shí)使用start,end,stride;如果非要使用,考慮兩次賦值(一個(gè)分片,一個(gè)調(diào)幅),或者使用內(nèi)置模塊itertoolsde 的 islice方法來(lái)進(jìn)行處理。

第 7 條:用列表推導(dǎo)來(lái)取代 map 和 filter 15

a = [1,2,3,4,5,6,7,8,9,10]
squares = [x*x for x in a]
squares = map(lambda x: x **2 ,a)

even_squares = [x**2 for x in a if x%2==0]
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()}
  • 列表表達(dá)式比內(nèi)置的map,filter更加清晰,因?yàn)?code>map,filter需要額外的lambda表達(dá)式的支持。
  • 列表表達(dá)式允許你很容易的跳過(guò)某些輸入值,而一個(gè)map沒(méi)有filter幫助的話(huà)就不能完成這一個(gè)功能。
  • 字典和集合也都支持列表表達(dá)式

第 8 條:不要使用含有兩個(gè)以上表達(dá)式的列表推導(dǎo) 16

matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]] 
squared = [[ x**2 for x in row] for row in matrix] 
flat = [x for row in matrix for x in row] 

my_lists = [
    [[1, 2, 3],[4, 5, 6]],
    # ...
]
# not good
flat = [ x for sublist in my_lists
          for sublist2 in sublist
          for x in sublist2]
# prefer
flat = []
for sublist in my_lists:
    for sublist2 in sublist:
        flat.append(sublist2)
  • 列表表達(dá)式支持多層的循環(huán)和條件語(yǔ)句,以及每層循環(huán)內(nèi)部的條件語(yǔ)句。
  • 當(dāng)列表表達(dá)式內(nèi)部多余兩個(gè)表達(dá)式的時(shí)候就會(huì)變得難于閱讀,這種寫(xiě)法應(yīng)該避免使用。

第 9 條:用生成器表達(dá)式來(lái)改寫(xiě)數(shù)據(jù)量較大的列表推導(dǎo) 18

列表生成式的缺點(diǎn)是,在推倒過(guò)程中,對(duì)輸入列表中的每一個(gè)值,可能都要?jiǎng)?chuàng)建只包含一個(gè)元素的新列表。這對(duì)于小的輸入序列可能是很好用的,但是大的輸入序列而言就很有可能導(dǎo)致你的程序崩潰。

ython提供了一個(gè)generator expression(生成器表達(dá)式),在程序運(yùn)行的過(guò)程中,生成其表達(dá)式不實(shí)現(xiàn)整個(gè)輸出序列,相反,生成其表達(dá)式僅僅是對(duì)從表達(dá)式中產(chǎn)生一個(gè)項(xiàng)目的迭代器進(jìn)行計(jì)算,說(shuō)白了就是每次僅僅處理一個(gè)迭代項(xiàng),而不是整個(gè)序列。

生成器表達(dá)式通過(guò)使用類(lèi)似于列表表達(dá)式的語(yǔ)法(在()之間而不是[]之間,僅此區(qū)別)來(lái)創(chuàng)建。

it = ( len(x) for x in open('/tmp/my_file.txt'))
print(next(it))
roots = ((x,x**0.5) for x in it)
print(next(roots))
  • 當(dāng)遇到大輸入事件的時(shí)候,使用列表表達(dá)式可能導(dǎo)致一些問(wèn)題。
  • 生成器表達(dá)式通過(guò)迭代的方式來(lái)處理每一個(gè)列表項(xiàng),可以防止出現(xiàn)內(nèi)存危機(jī)。
  • 當(dāng)生成器表達(dá)式 處于鏈?zhǔn)綘顟B(tài)時(shí),會(huì)執(zhí)行的很迅速。

第 10 條:盡量用 enumerate 取代 range 20

# good
for i, flavor in enumerate(flavor_list):
    print(‘%d: %s’ % (i + 1, flavor))
    
# not good
for i in range(len(flavor_list)):
    flavor = flavor_list[i]
        print(‘%d: %s’ % (i + 1, flavor))
  • enumerate提供了簡(jiǎn)潔的語(yǔ)法,再循環(huán)迭代一個(gè)迭代器的同時(shí)既能獲取下標(biāo),也能獲取當(dāng)前值。
  • 可以添加第二個(gè)參數(shù)來(lái)指定索引開(kāi)始的序號(hào),默認(rèn)為0

第 11 條:用 zip 函數(shù)同時(shí)遍歷兩個(gè)迭代器 21

names = ['Cecilia','Lise','Marie'] 
letters = [len(n) for n in names] 
for name, count in zip(names, letters): 
    if count > max_letters: 
        longest_name = name 
        max_letters = count
  • 內(nèi)置的zip函數(shù)可以并行的對(duì)多個(gè)迭代器進(jìn)行處理。
  • 在Python3中,zip 相當(dāng)于生成器,會(huì)在遍歷過(guò)程中逐次產(chǎn)生元組。而在Python2中,zip返回的是一個(gè)包含了其處理好的所有元祖的一個(gè)集合。
  • 如果所處理的迭代器的長(zhǎng)度不一致時(shí),zip會(huì)默認(rèn)截?cái)噍敵?,使得長(zhǎng)度為最先到達(dá)尾部的那個(gè)長(zhǎng)度。
  • 內(nèi)置模塊itertools中的zip_longest函數(shù)可以并行地處理多個(gè)迭代器,而可以無(wú)視長(zhǎng)度不一致的問(wèn)題。

第 12 條:不要在 for 和 while 循環(huán)后面寫(xiě) else 塊 23

for i in range(2):
    print('Loop %d' % i)
else:
    print('Else block')
>>>
Loop 0
Loop 1
Else block
  • Python有用特殊的語(yǔ)法能夠讓else語(yǔ)塊在循環(huán)體結(jié)束的時(shí)候立刻得到執(zhí)行。
  • 循環(huán)體后的else語(yǔ)塊只有在循環(huán)體沒(méi)有觸發(fā)break語(yǔ)句的時(shí)候才會(huì)執(zhí)行。
  • 避免在循環(huán)體的后面使用else語(yǔ)塊,因?yàn)檫@樣的表達(dá)不直觀,而且容易誤導(dǎo)讀者。

第 13 條:合理利用 try/except/else/f inally 結(jié)構(gòu)中的每個(gè)代碼塊 25

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
  • try/finally組合語(yǔ)句可以使得你的代碼變得很整潔而無(wú)視try塊中是否發(fā)生異常。
  • else塊可以最大限度的減少try塊中的代碼的長(zhǎng)度,并且可以可視化地辨別try/except成功運(yùn)行的部分。
  • else塊經(jīng)常會(huì)被用于在try塊成功運(yùn)行后添加額外的行為,但是要確保代碼會(huì)在finally塊之前得到運(yùn)行。

finally 塊: 總是會(huì)執(zhí)行,可以用來(lái)關(guān)閉文件句柄之類(lèi)的
else 塊 : try 塊沒(méi)有發(fā)生異常則執(zhí)行 else 塊,有了 else 塊,我們可以盡量減少 try 塊的代碼量

第 2 章 函數(shù) 28

第 14 條:盡量用異常來(lái)表示特殊情況,而不要返回 None 28

  • 返回None的函數(shù)來(lái)作為特殊的含義是容易出錯(cuò)的,因?yàn)?code>None和其他的變量(例如 zero,空字符串)在條件表達(dá)式的判斷情景下是等價(jià)的。
  • 通過(guò)觸發(fā)一個(gè)異常而不是直接的返回None是比較常用的一個(gè)方法。這樣調(diào)用方就能夠合理地按照函數(shù)中的說(shuō)明文檔來(lái)處理由此而引發(fā)的異常了。
def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError('Invalid inputs') from e
        
# use like this:        
x, y = 5, 2
try:
    result = divide(x, y)
except ValueError:
    print("Invalid inputs")
else:
    print("Result is %.1f"% result)

第 15 條:了解如何在閉包里使用外圍作用域中的變量 30

Python支持閉包。閉包是一種定義在某個(gè)作用域中的函數(shù),這種函數(shù)引用了作用域中的變量。
Python的函數(shù)是一級(jí)對(duì)象,我們可以直接引用函數(shù),把函數(shù)賦值給變量、當(dāng)成參數(shù)傳遞給其它函數(shù)等等。。

Python編譯器變量查找域的順序:

  • 當(dāng)前函數(shù)的作用域
  • 任何其他的封閉域(比如其他的包含著的函數(shù))。
  • 包含該段代碼的模塊域(也稱(chēng)之為全局域)
  • 內(nèi)置域(包含了像len,str等函數(shù)的域)
# 優(yōu)先排序
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的作用域就會(huì)由helper轉(zhuǎn)至sort_priority2函數(shù)

    values.sort(key=helper)
    return found
  1. 獲取閉包中的數(shù)據(jù)-Python3

    def srt_priority3(numbers, group):
        found = False
        def helper(x):
            nonlocal found  # 表明found是閉包外數(shù)據(jù)
            if x in group:
                found = True
                return (0, x)
            return (1, x)
        numbers.sort(key=helper)
        return found
    

    當(dāng)數(shù)據(jù)在閉包外將被賦值到另一個(gè)域時(shí),nonlocal 語(yǔ)句使得這個(gè)過(guò)程變得很清晰。它也是對(duì)global語(yǔ)句的一個(gè)補(bǔ)充,可以明確的表明變量的賦值應(yīng)該被直接放置到模塊域中。

    然而,像這樣的反模式,對(duì)使用在那些簡(jiǎn)單函數(shù)之外的其他的任何地方。nonlocal引起的副作用是難以追蹤的,而在那些包含著nonlocal語(yǔ)句和賦值語(yǔ)句交叉聯(lián)系的大段代碼的函數(shù)的內(nèi)部則尤為明顯。

    當(dāng)你感覺(jué)自己的nonlocal語(yǔ)句開(kāi)始變的復(fù)雜的時(shí)候,我非常建議你重構(gòu)一下代碼,寫(xiě)成一個(gè)工具類(lèi)。這里,我定義了一個(gè)實(shí)現(xiàn)了與上面的那個(gè)函數(shù)功能相一致的工具類(lèi)。雖然有點(diǎn)長(zhǎng),但是代碼卻變得更加的清晰了(詳見(jiàn)第23項(xiàng):對(duì)于簡(jiǎn)單接口使用函數(shù)而不是類(lèi)里面的__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
    
  2. Python2中的作用域

    Python2是不支持nonlocal關(guān)鍵字的。為了實(shí)現(xiàn)相似的功能,你需要廣泛的借助于Python的作用與域規(guī)則。雖然這個(gè)方法并不是完美的,但是這是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 將會(huì)橫向查找該變量所在的域來(lái)分析其當(dāng)前值。技巧就是發(fā)現(xiàn)的值是一個(gè)易變的列表。這意味著一旦檢索,閉包就可以修改found的狀態(tài)值,并且把內(nèi)部數(shù)據(jù)的改變發(fā)送到外部,這也就打破了閉包引發(fā)的局部變量作用域無(wú)法被改變的難題。其根本還是在于列表本身元素值可以被改變,這才是此函數(shù)可以正常工作的關(guān)鍵。

    當(dāng)found為一個(gè)dictionary類(lèi)型的時(shí)候,也是可以正常工作的,原理與上文所言一致。此外,found還可以是一個(gè)集合,一個(gè)你自定義的類(lèi)等等。

要點(diǎn):

  • 閉包函數(shù)可以從變量被定義的作用域內(nèi)引用變量。
  • 默認(rèn)地,閉包不能通過(guò)賦值來(lái)影響其檢索域。
  • 在Python3中,可以使用nonlocal關(guān)鍵字來(lái)突破閉包的限制,進(jìn)而在其檢索域內(nèi)改變其值。(global 關(guān)鍵字用于使用全局變量,nonlocal 關(guān)鍵字用于使用局部變量(函數(shù)內(nèi)))
  • Python2中沒(méi)有nonlocal關(guān)鍵字,替代方案就是使用一個(gè)單元素(如列表,字典,集合等等)來(lái)實(shí)現(xiàn)與nonlocal一致的功能。
  • 除了簡(jiǎn)單的函數(shù),在其他任何地方都應(yīng)該盡力的避免使用nonlocal關(guān)鍵字。

第 16 條:考慮用生成器來(lái)改寫(xiě)直接返回列表的函數(shù) 35

  • 相較于返回一個(gè)列表的情況,替代方案中使用生成器可以使得代碼變得更加的清晰。
  • 生成器返回的迭代器,是在其生成器內(nèi)部一個(gè)把值傳遞給了yield變量的集合。
  • 生成器可以處理很大的輸出序列就是因?yàn)樗谔幚淼臅r(shí)候不會(huì)完全的包含所有的數(shù)據(jù)。
#list
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))

第 17 條:在參數(shù)上面迭代時(shí),要多加小心 37

  • 多次遍歷輸入?yún)?shù)的時(shí)候應(yīng)該多加小心。如果參數(shù)是迭代器的話(huà)你可能看到奇怪的現(xiàn)象或者缺少值現(xiàn)象的發(fā)生。
  • Python的iterator協(xié)議定義了容器和迭代器在iter和next下對(duì)于循環(huán)和相關(guān)表達(dá)式的關(guān)系。
    只要實(shí)現(xiàn)了__iter__方法,你就可以很容易的定義一個(gè)可迭代的容器類(lèi)。
  • 通過(guò)連續(xù)調(diào)用兩次iter方法,你就可以預(yù)先檢測(cè)一個(gè)值是不是迭代器而不是容器。兩次結(jié)果一致那就是迭代器,否則就是容器了。調(diào)用內(nèi)置的next函數(shù),可以令迭代器前進(jìn)一步。
#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)) # 這里其實(shí)已經(jīng)執(zhí)行到頭了
>>>
[15, 35, 80]
[]
# 如何解決?每次調(diào)用都創(chuàng)建iterator避免上面list分配內(nèi)存
def normalize_func(get_iter):  # get_iter 是函數(shù)
    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循環(huán)會(huì)調(diào)用內(nèi)置iter函數(shù),進(jìn)而調(diào)用對(duì)象的__iter__方法,__iter__會(huì)返回iterator對(duì)象(實(shí)現(xiàn)__next__方法)。用iter函數(shù)檢測(cè)iterator:

class ReadVists(object):
    def __int__(self, data_path):
        self.data_path = data_path
    # 在自己的類(lèi)中把__iter__實(shí)現(xiàn)為生成器,就可以實(shí)現(xiàn)一個(gè)可以迭代的容器類(lèi)
    def __iter__(self):
        with open(self.data_path) as f:
            for line in f:
                yield int(line)

def normalize_defensive(numbers):
    if iter(numbers) is iter(numbers): # 是個(gè)迭代器
        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) # no error
visits = ReadVIsitors(path)
normalize_defensive(visits) # no error

# 但是如果輸入值不是一個(gè)容器類(lèi)的話(huà),就會(huì)引發(fā)異常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container

第 18 條:用數(shù)量可變的位置參數(shù)減少視覺(jué)雜訊 41

  • 通過(guò)使用*args定義語(yǔ)句,函數(shù)可以接收可變數(shù)量的位置參數(shù)。
  • 你可以通過(guò)*操作符來(lái)將序列中的元素作為位置變量。
  • 帶有*操作符的生成器變量可能會(huì)引起程序的內(nèi)存溢出,或者機(jī)器宕機(jī)。
  • 為可以接受*args的函數(shù)添加新的位置參數(shù)可以產(chǎn)生難于發(fā)現(xiàn)的問(wèn)題,應(yīng)該謹(jǐn)慎使用。
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 條:用關(guān)鍵字參數(shù)來(lái)表達(dá)可選的行為 43

  • 函數(shù)的參數(shù)值即可以通過(guò)位置被指定,也可以通過(guò)關(guān)鍵字來(lái)指定。
  • 相較于使用位置參數(shù)賦值,使用關(guān)鍵字來(lái)賦值會(huì)讓你的賦值語(yǔ)句邏輯變得更加的清晰。
  • 帶有默認(rèn)參數(shù)的關(guān)鍵字參數(shù)函數(shù)可以很容易的添加新的行為,尤其適合向后兼容。
  • 可選的關(guān)鍵字參數(shù)應(yīng)該優(yōu)于位置參數(shù)被考慮使用。

關(guān)鍵字參數(shù)的好處:

  1. 代碼可讀性的提高
  2. 以在定義的時(shí)候初始化一個(gè)默認(rèn)值
  3. 在前面的調(diào)用方式不變的情況下可以很好的拓展函數(shù)的參數(shù),不用修改太多的代碼
# before
def flow_rate(weight_diff, time_diff, period=1):
    return (weight_diff / time_diff) * period

# after
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 和文檔字符串來(lái)描述具有動(dòng)態(tài)默認(rèn)值的參數(shù) 46

  • 默認(rèn)參數(shù)只會(huì)被賦值一次:在其所在模塊被加載的過(guò)程中,這有可能導(dǎo)致一些奇怪的現(xiàn)象。
  • 使用None作為關(guān)鍵字參數(shù)的默認(rèn)值會(huì)有一個(gè)動(dòng)態(tài)值。要在該函數(shù)的說(shuō)明文檔中詳細(xì)的記錄一下。
def log(message, when=datetime.now()):
    print(‘%s: %s’ % (when, message))

log(‘Hi there!’)
sleep(0.1)
log(‘Hi again!’)
>>>
2019-1-14 22:10:10.371432: Hi there!
2019-1-14 22:10:10.371432: Hi again!   # 時(shí)間并沒(méi)有變化。
     
# 使用None作為默認(rèn)值,文檔里要有說(shuō)明
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))
    
# another example
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

第 21 條:用只能以關(guān)鍵字形式指定的參數(shù)來(lái)確保代碼明晰 49

  • 關(guān)鍵字參數(shù)使得函數(shù)調(diào)用的意圖更加的清晰,明顯。
  • 使用keyword-only參數(shù)可以強(qiáng)迫函數(shù)調(diào)用者提供關(guān)鍵字來(lái)賦值,這樣對(duì)于容易使人疑惑的函數(shù)參數(shù)很有效,尤其適用于接收多個(gè)布爾變量的情況。
  • Python3中有明確的keyword-only函數(shù)語(yǔ)法。
  • Python2中可以通過(guò)**kwargs模擬實(shí)現(xiàn)keyword-only函數(shù)語(yǔ)法,并且人工的觸發(fā)TypeError異常。
  • keyword-only在函數(shù)參數(shù)列表中的位置很重要,這點(diǎn)大家尤其應(yīng)該明白!
def safe_division(number, divisor, ignore_overflow,
                  ignore_zero_division):
    # 省略了實(shí)現(xiàn).......
result = safe_division(1, 10**500, True, False)
result = safe_division(1, 0, False, True)
# 上述函數(shù)使用上不方便,因?yàn)槿菀淄?ignore_overflow 和 ignore_zero_division 的順序

# 用 keyword 引數(shù)可解決此問(wèn)題,在 Python 3 可以宣告強(qiáng)制接收 keyword-only 參數(shù)。
def safe_division_c(number, divisor, *,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    #....

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 雖然沒(méi)有這種語(yǔ)法,但可以用 ** 操作符模擬
注:* 操作符接收可變數(shù)量的位置參數(shù),** 接受任意數(shù)量的關(guān)鍵字參數(shù)

# 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)
    # ···

# test
safe_division(1, 10)
safe_division(1, 0, ignore_zero_division=True)
safe_division(1, 10**500, ignore_overflow=True)
# 而想通過(guò)位置參數(shù)賦值,就不會(huì)正常的運(yùn)行了
safe_division(1, 0, False, True)
>>>
TypeError:safe_division() takes 2 positional arguments but 4 were given.

第 3 章 類(lèi)與繼承 53

第 22 條:盡量用輔助類(lèi)來(lái)維護(hù)程序的狀態(tài),而不要用字典和元組 53

  • 避免字典中嵌套字典,或者長(zhǎng)度較大的元組。
  • 在一個(gè)整類(lèi)(類(lèi)似于前面第一個(gè)復(fù)雜類(lèi)那樣)之前考慮使用 namedtuple 制作輕量,不易發(fā)生變化的容器。
  • 當(dāng)內(nèi)部的字典關(guān)系變得復(fù)雜的時(shí)候?qū)⒋a重構(gòu)到多個(gè)工具類(lèi)中。

dictionaries 以及 tuples 拿來(lái)存簡(jiǎn)單的資料很方便,但是當(dāng)資料越來(lái)越復(fù)雜時(shí),例如多層 dictionaries 或是 n-tuples,程式的可讀性就下降了。你可以從依賴(lài)樹(shù)的底端開(kāi)始,將其劃分成多個(gè)類(lèi)。這就是代碼的設(shè)計(jì)問(wèn)題了。

第 23 條:簡(jiǎn)單的接口應(yīng)該接受函數(shù),而不是類(lèi)的實(shí)例 58

  • 在Python中,不需要定義或?qū)崿F(xiàn)什么類(lèi),對(duì)于簡(jiǎn)單接口組件而言,函數(shù)就足夠了。
  • Python中引用函數(shù)和方法的原因就在于它們是first-class,可以直接的被運(yùn)用在表達(dá)式中。
    特殊方法__call__允許你像調(diào)用函數(shù)一樣調(diào)用一個(gè)對(duì)象實(shí)例。
  • 當(dāng)你需要一個(gè)函數(shù)來(lái)維護(hù)狀態(tài)信息的時(shí)候,考慮一個(gè)定義了__call__方法的狀態(tài)閉包類(lèi)哦(詳見(jiàn)第15項(xiàng):了解閉包是怎樣與變量作用域的聯(lián)系)

Python中的許多內(nèi)置的API都允許你通過(guò)向函數(shù)傳遞參數(shù)來(lái)自定義行為。這些被API使用的hooks將會(huì)在它們運(yùn)行的時(shí)候回調(diào)給你的代碼。例如:list類(lèi)型的排序方法中有一個(gè)可選的key 參數(shù)來(lái)決定排序過(guò)程中每個(gè)下標(biāo)的值。這里,我使用一個(gè)lambda表達(dá)式作為這個(gè)鍵鉤子,根據(jù)名字中字符的長(zhǎng)度來(lái)為這個(gè)集合排序。

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

函數(shù)可以作為鉤子來(lái)工作是因?yàn)?code>Python有first-class函數(shù):在編程的時(shí)候函數(shù),方法可以像其他的變量值一樣被引用,或者被傳遞給其他的函數(shù)。Python允許類(lèi)來(lái)定義__call__這個(gè)特殊的方法。它允許一個(gè)對(duì)象像被函數(shù)一樣來(lái)被調(diào)用。這樣的一個(gè)實(shí)例也引起了callable這個(gè)內(nèi)True的事實(shí)。

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)
# 這里我使用一個(gè)BetterCountMissing實(shí)例作為defaultdict函數(shù)的默認(rèn)的hook值來(lái)追蹤缺省值被添加的次數(shù)。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
    result[key] += amount
assert counter.added == 2

第 24 條:以 @classmethod 形式的多態(tài)去通用地構(gòu)建對(duì)象 62

  • Python的每個(gè)類(lèi)只支持單個(gè)的構(gòu)造方法,__init__
  • 使用@classmethod可以為你的類(lèi)定義可替代構(gòu)造方法的方法。
  • 類(lèi)的多態(tài)為具體子類(lèi)的組合提供了一種更加通用的方式。

使用 @classmethod起到多態(tài)的效果:一個(gè)對(duì)于分層良好的類(lèi)樹(shù)中,不同類(lèi)之間相同名稱(chēng)的方法卻實(shí)現(xiàn)了不同的功能的體現(xiàn)。 下面的函數(shù) generate_inputs() 不夠一般化,只能使用 PathInputData ,如果想使用其它 InputData 的子類(lèi),必須改變函數(shù)。

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))

問(wèn)題在于建立 InputData 子類(lèi)的物件不夠一般化,如果你想要編寫(xiě)另一個(gè) InputData 的子類(lèi)就必須重寫(xiě) read 方法幸好有 @classmethod,可以達(dá)到一樣的效果。

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 初始化父類(lèi) 67

  • Python的解決實(shí)例化次序問(wèn)題的方法MRO解決了菱形繼承中超類(lèi)多次被初始化的問(wèn)題。
  • 總是應(yīng)該使用super來(lái)初始化父類(lèi)。

應(yīng)該避免棱形繼承。如有會(huì)發(fā)生,說(shuō)明設(shè)計(jì)的不夠好。

第 26 條:只在使用 Mix-in 組件制作工具類(lèi)時(shí)進(jìn)行多重繼承 71

  • 如果可以使用mix-in實(shí)現(xiàn)相同的結(jié)果輸出的話(huà),就不要使用多繼承了。
  • 當(dāng)mix-in類(lèi)需要的時(shí)候,在實(shí)例級(jí)別上使用可插拔的行為可以為每一個(gè)自定義的類(lèi)工作的更好。
  • 從簡(jiǎn)單的行為出發(fā),創(chuàng)建功能更為靈活的mix-in。

如果你發(fā)現(xiàn)自己渴望隨繼承的便利和封裝,那么考慮mix-in吧。它是一個(gè)只定義了幾個(gè)類(lèi)必備功能方法的很小的類(lèi)。Mix-in類(lèi)不定義以自己的實(shí)例屬性,也不需要它們的初始化方法init被調(diào)用。Mix-in可以被分層和組織成最小化的代碼塊,方便代碼的重用。

mix-in 是可以替換的 class ,通常只定義 methods ,雖然本質(zhì)上上還是通過(guò)繼承的方式,但因?yàn)?mix-in 沒(méi)有自己的 state ,也就是說(shuō)沒(méi)有定義 attributes ,使用上更有彈性。

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
    
    # hasattr 函數(shù)動(dòng)態(tài)訪問(wèn)屬性,isinstance 函數(shù)動(dòng)態(tài)檢測(cè)對(duì)象類(lèi)型
    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對(duì)象轉(zhuǎn)換到一個(gè)字典中變得容易多了。
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
}

第 27 條:多用 public 屬性,少用 private 屬性 75

  • Python 編譯器無(wú)法嚴(yán)格保證 private 字段的私密性
  • 不要盲目將屬性設(shè)置為 private,而是應(yīng)該從一開(kāi)始就做好規(guī)劃,并允子類(lèi)更多地訪問(wèn)超類(lèi)的內(nèi)部的API
  • 應(yīng)該多用 protected 屬性,并且在文檔中把這些字段的合理用法告訴子類(lèi)的開(kāi)發(fā)者,而不要試圖用 private 屬性來(lái)限制子類(lèi)的訪問(wèn)
  • 只有當(dāng)子類(lèi)不受自己控制的時(shí)候,才可以考慮使用 private 屬性來(lái)避免名稱(chēng)沖突

Python 里面沒(méi)有真正的 “private variable”,想存取都可以存取得到。一般來(lái)說(shuō) Python 慣例是在變數(shù)前加一個(gè)底線(xiàn)代表 protected variable ,作用在于提醒開(kāi)發(fā)者使用上要注意。雙底線(xiàn)的命名方式是為了避免父類(lèi)和子類(lèi)間的命名沖突,除此之外盡量避免使用這種命名。

第 28 條:繼承 collections.abc 以實(shí)現(xiàn)自定義的容器類(lèi)型 79

  • 如果要定制的子類(lèi)比較簡(jiǎn)單,那就可以直接從Python的容器類(lèi)型(如list或dict)中繼承
  • 想正確實(shí)現(xiàn)自定義的容器類(lèi)型,可能需要編寫(xiě)大量的特殊方法
  • 編寫(xiě)自制的容器類(lèi)型時(shí),可以從collection.abc 模塊的抽象類(lèi)基類(lèi)中繼承,那些基類(lèi)能確保我們的子類(lèi)具備適當(dāng)?shù)慕涌诩靶袨?/li>

ollections.abc 里面的 abstract classes 的作用是讓開(kāi)發(fā)者方便地開(kāi)發(fā)自己的 container ,例如 list。一般情況下繼承l(wèi)ist 就ok了,但是當(dāng)結(jié)構(gòu)比較復(fù)雜的時(shí)候就需要自己自定義,例如 list 有許多 方法,要一一實(shí)現(xiàn)有點(diǎn)麻煩。

但是使用者可能想使用像 count()以及 index()等 list 的 方法 ,這時(shí)候可以使用 collections.abc的 Sequence 。子類(lèi)只要實(shí)現(xiàn) __getitem__以及 __len__, Sequence 以及提供count()以及 index()了,而且如果子類(lèi)沒(méi)有實(shí)現(xiàn)類(lèi)似 Sequence 的抽象基類(lèi)所要求的每個(gè)方法,collections.abc 模塊就會(huì)指出這個(gè)錯(cuò)誤。

第 4 章 元類(lèi)及屬性 84

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

  • 使用public屬性避免set和get方法,@property定義一些特別的行為
  • 如果訪問(wèn)對(duì)象的某個(gè)屬性的時(shí)候,需要表現(xiàn)出特殊的行為,那就用@property來(lái)定義這種行為
  • @property 方法應(yīng)該遵循最小驚訝原則,而不應(yīng)該產(chǎn)生奇怪的副作用
  • 確保@property方法是快速的,如果是慢或者復(fù)雜的工作應(yīng)該放在正常的方法里面
# 不要把 java 的那一套 getter 和 setter 帶進(jìn)來(lái)
# not like this:
lass OldResistor(object):
    def __init__(self, ohms):
        self._ohms = ohms

    def get_ohms(self):
        return self._ohms

    def set_ohms(self, ohms):
        self._ohms = ohms

# just like this:
class Resistor(object):
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0

使用@property,來(lái)綁定一些特殊操作,但是不要產(chǎn)生奇怪的副作用,比如在getter里面做一些賦值的操作

class VoltageResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
        self._voltage = 0

    # 相當(dāng)于 getter
    @property
    def voltage(self):
        return self._voltage

    # 相當(dāng)于 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)
# 會(huì)執(zhí)行 setter 方法
r2.voltage = 10
print('After:  %5r amps' % r2.current)

第 30 條:考慮用 @property 來(lái)代替屬性重構(gòu) 88

  • 使用@property給已有屬性擴(kuò)展新需求
  • 可以用 @property 來(lái)逐步完善數(shù)據(jù)模型
  • 當(dāng)@property太復(fù)雜了才考慮重構(gòu)

@property可以把簡(jiǎn)單的數(shù)值屬性遷移為實(shí)時(shí)計(jì)算,只定義 getter 不定義 setter 那么就是一個(gè)只讀屬性

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

這種寫(xiě)法的好處就在于:從前使用的Bucket.quota 的那些舊代碼,既不需要做出修改,也不需要擔(dān)心現(xiàn)在的Bucket類(lèi)是如何實(shí)現(xiàn)的,可以輕松無(wú)痛擴(kuò)展新功能。但是@property也不能濫用,而且@property的一個(gè)缺點(diǎn)就是無(wú)法被復(fù)用,同一套邏輯不能在不同的屬性之間重復(fù)使用如果不停的編寫(xiě)@property方法,那就意味著當(dāng)前這個(gè)類(lèi)的代碼寫(xiě)的確實(shí)很糟糕,此時(shí)應(yīng)該重構(gòu)了。

第 31 條:用描述符來(lái)改寫(xiě)需要復(fù)用的 @property 方法 92

  • 如果想復(fù)用 @property 方法及其驗(yàn)證機(jī)制,那么可以自定義描述符類(lèi)
  • WeakKeyDictionary 可以保證描述符類(lèi)不會(huì)泄露內(nèi)存
  • 通過(guò)描述符協(xié)議來(lái)實(shí)現(xiàn)屬性的獲取和設(shè)置操作時(shí),不要糾結(jié)于__getatttttribute__ 的方法的具體運(yùn)作細(xì)節(jié)

property最大的問(wèn)題是可能造成 duplicated code 這種 code smell. 可以使用 descriptor 解決,下面的程式將重復(fù)的邏輯封裝在 Grade 里面。但是這個(gè)程式根本不能用 ,因?yàn)榇嫒〉降氖?class attributes,例如 exam.writing_grade = 40其實(shí)是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

解決方式是用個(gè) dictionary 存起來(lái),這里使用 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__ 實(shí)現(xiàn)按需生成的屬性 97

  • 通過(guò)__getttattr____setattr__,我們可以用惰性的方式來(lái)加載并保存對(duì)象的屬性
  • 要理解 __getattr____getattribute__ 的區(qū)別:前者只會(huì)在待訪問(wèn)的屬性缺失時(shí)觸發(fā),而后者則會(huì)在每次訪問(wèn)屬性的時(shí)候觸發(fā)
  • 如果要在__getattributte____setattr__ 方法中訪問(wèn)實(shí)例屬性,那么應(yīng)該直接通過(guò) super() 來(lái)做,以避免無(wú)限遞歸
  • obj.name,getattr和hasattr都會(huì)調(diào)用getattribute方法,如果name不在obj.dict里面,還會(huì)調(diào)用getattr方法,如果沒(méi)有自定義getattr方法會(huì)AttributeError異常
  • 只要有賦值操作(=,setattr)都會(huì)調(diào)用setattr方法(包括a = A())

__getattr____getattribute__都可以動(dòng)態(tài)地存取 attributes ,不同點(diǎn)在于如果 __dict__找不到才會(huì)呼叫 __getattr__,而 __getattribute__每次都會(huì)被呼叫到。

第 33 條:用元類(lèi)來(lái)驗(yàn)證子類(lèi) 102

  • 通過(guò)元類(lèi),我們可以在生成子類(lèi)對(duì)象之前,先驗(yàn)證子類(lèi)的定義是否合乎規(guī)范
  • Python2 和 Python3 指定元類(lèi)的語(yǔ)法略有不同
  • 使用元類(lèi)對(duì)類(lèi)型對(duì)象進(jìn)行驗(yàn)證
  • Python 系統(tǒng)把子類(lèi)的整個(gè) class 語(yǔ)句體處理完畢之后,就會(huì)調(diào)用其元類(lèi)的__new__ 方法

第 34 條:用元類(lèi)來(lái)注冊(cè)子類(lèi) 104

  • 在構(gòu)建模塊化的 Python 程序時(shí)候,類(lèi)的注冊(cè)是一種很有用的模式
  • 開(kāi)發(fā)者每次從基類(lèi)中繼承子類(lèi)的時(shí),基類(lèi)的元類(lèi)都可以自動(dòng)運(yùn)行注冊(cè)代碼
  • 通過(guò)元類(lèi)來(lái)實(shí)現(xiàn)類(lèi)的注冊(cè),可以確保所有子類(lèi)都不會(huì)泄露,從而避免后續(xù)的錯(cuò)誤

第 35 條:用元類(lèi)來(lái)注解類(lèi)的屬性 108

  • 借助元類(lèi),我們可以在某個(gè)類(lèi)完全定義好之前,率先修改該類(lèi)的屬性
  • 描述符與元類(lèi)能夠有效的組合起來(lái),以便對(duì)某種行為做出修飾,或者在程序運(yùn)行時(shí)探查相關(guān)信息
  • 如果把元類(lèi)與描述符相結(jié)合,那就可以在不使用 weakerf 模塊的前提下避免內(nèi)存泄露

第 5 章 并發(fā)及并行 112

第 36 條:用 subprocess 模塊來(lái)管理子進(jìn)程 113

  • 使用 subprocess 模塊運(yùn)行子進(jìn)程管理自己的輸入和輸出流
  • subprocess 可以并行執(zhí)行最大化CPU的使用
  • communicate 的 timeout 參數(shù)避免死鎖和被掛起的子進(jìn)程
import subprocess
import os

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

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

第 37 條:可以用線(xiàn)程來(lái)執(zhí)行阻塞式 I/O,但不要用它做平行計(jì)算 117

  • 因?yàn)镚IL,Python thread并不能并行運(yùn)行多段代碼
  • Python保留thread的兩個(gè)原因:1.可以模擬多線(xiàn)程,2.多線(xiàn)程可以處理I/O阻塞的情況
  • Python thread可以并行執(zhí)行多個(gè)系統(tǒng)調(diào)用,這使得程序能夠在執(zhí)行阻塞式I/O操作的同時(shí),執(zhí)行一些并行計(jì)算

第 38 條:在線(xiàn)程中使用 Lock 來(lái)防止數(shù)據(jù)競(jìng)爭(zhēng) 121

  • 雖然Python thread不能同時(shí)執(zhí)行,但是Python解釋器還是會(huì)打斷操作數(shù)據(jù)的兩個(gè)字節(jié)碼指令,所以還是需要鎖
  • thread模塊的Lock類(lèi)是Python的互斥鎖實(shí)現(xiàn)
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

def worker(sensor_index, how_many, counter):
    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()

how_many = 10**5
counter = LockingCounter()
run_threads(worker, how_many, counter)
print('Counter should be %d, found %d' %
      (5 * how_many, counter.count))

第 39 條:用 Queue 來(lái)協(xié)調(diào)各線(xiàn)程之間的工作 124

  • 管線(xiàn)是一種優(yōu)秀的任務(wù)處理方式,它可以把處理流程劃分為若干階段,并使用多條Python線(xiàn)程同時(shí)執(zhí)行這些任務(wù)
  • 構(gòu)建并發(fā)式的管線(xiàn)時(shí),要注意許多問(wèn)題,包括:如何防止某個(gè)階段陷入持續(xù)等待的狀態(tài)之中、如何停止工作線(xiàn)程,以及如何防止內(nèi)存膨脹等
  • Queue類(lèi)具備構(gòu)建健壯并發(fā)管道的特性:阻塞操作,緩存大小和連接(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 條:考慮用協(xié)程來(lái)并發(fā)地運(yùn)行多個(gè)函數(shù) 131

  • 線(xiàn)程有三個(gè)大問(wèn)題:
    • 需要特定工具去確定安全性
    • 單個(gè)線(xiàn)程需要8M的內(nèi)存
    • 線(xiàn)程啟動(dòng)消耗
  • coroutine只有1kb的內(nèi)存消耗
  • generator可以通過(guò)send方法把值傳遞給yield
def my_coroutine():
  while True:
      received = yield
      print("Received:", received)
it = my_coroutine()
next(it)
it.send("First")
('Received:', 'First')

第 41 條:考慮用 concurrent.futures 來(lái)實(shí)現(xiàn)真正的平行計(jì)算 141

  • CPU瓶頸模塊使用C擴(kuò)展
  • concurrent.futures的multiprocessing可以并行處理一些任務(wù),Python2沒(méi)有這個(gè)模塊
  • multiprocessing 模塊所提供的那些高級(jí)功能,都特別復(fù)雜,開(kāi)發(fā)者盡量不要直接使用它們

使用 concurrent.futures 里面的 ProcessPoolExecutor 可以很簡(jiǎn)單地平行處理 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))

第 6 章 內(nèi)置模塊 145
第 42 條:用 functools.wraps 定義函數(shù)修飾器 145
第 43 條:考慮以 contextlib 和 with 語(yǔ)句來(lái)改寫(xiě)可復(fù)用的 try/f inally 代碼 148
第 44 條:用 copyreg 實(shí)現(xiàn)可靠的 pickle 操作 151
第 45 條:應(yīng)該用 datetime 模塊來(lái)處理本地時(shí)間,而不是用 time 模塊 157
第 46 條:使用內(nèi)置算法與數(shù)據(jù)結(jié)構(gòu) 161
第 47 條:在重視精確度的場(chǎng)合,應(yīng)該使用 decimal 166
第 48 條:學(xué)會(huì)安裝由 Python 開(kāi)發(fā)者社區(qū)所構(gòu)建的模塊 168

第 7 章 協(xié)作開(kāi)發(fā) 170
第 49 條:為每個(gè)函數(shù)、類(lèi)和模塊編寫(xiě)文檔字符串 170
第 50 條:用包來(lái)安排模塊,并提供穩(wěn)固的 API 174
第 51 條:為自編的模塊定義根異常,以便將調(diào)用者與 API 相隔離 179
第 52 條:用適當(dāng)?shù)姆绞酱蚱蒲h(huán)依賴(lài)關(guān)系 182
第 53 條:用虛擬環(huán)境隔離項(xiàng)目,并重建其依賴(lài)關(guān)系 187

第 8 章 部署 193
第 54 條:考慮用模塊級(jí)別的代碼來(lái)配置不同的部署環(huán)境 193
第 55 條:通過(guò) repr 字符串來(lái)輸出調(diào)試信息 195
第 56 條:用 unittest 來(lái)測(cè)試全部代碼 198
第 57 條:考慮用 pdb 實(shí)現(xiàn)交互調(diào)試 201
第 58 條:先分析性能,然后再優(yōu)化 203
第 59 條:用 tracemalloc 來(lái)掌握內(nèi)存的使用及泄漏情況 208

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容