1. 程式人生 > >編寫高質量Python程式碼的59個有效方法

編寫高質量Python程式碼的59個有效方法

這個週末斷斷續續的閱讀完了《Effective Python之編寫高質量Python程式碼的59個有效方法》,感覺還不錯,具有很大的指導價值。 下面將以最簡單的方式記錄這59條建議,並在大部分建議後面加上了說明和示例,文章篇幅大,請您提前備好瓜子和啤酒!

Python學習資料或者需要程式碼、視訊加Python學習群:960410445

1. 用Pythonic方式思考

第一條:確認自己使用的Python版本

(1)有兩個版本的python處於活躍狀態,python2和python3

(2)有很多流行的Python執行時環境,CPython、Jython、IronPython以及PyPy等

(3)在開發專案時,應該優先考慮Python3

第二條:遵循PEP風格指南

PEP8是針對Python程式碼格式而編訂的風格指南,參考: http://www.python.org/dev/peps/pep-0008

(1)當編寫Python程式碼時,總是應該遵循PEP8風格指南

(2)當廣大Python開發者採用同一套程式碼風格,可以使專案更利於多人協作

(3)採用一致的風格來編寫程式碼,可以令後續的修改工作變得更為容易

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

(1)python2提供str個unicode,python3中修改為bytes和str,bytes為原始的8位值,str包含unicode字元,在進行編碼轉換時使用decode和encode方法

(2)從檔案中讀取二進位制資料,或向其中寫入二進位制資料時,總應該以‘rb’或‘wb’等二進位制模式來開啟檔案

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

(1)開發者很容易過度運用Python的語法特性,從而寫出那種特別複雜並且難以理解的單行表示式

(2)請把複雜的表示式移入輔助函式中,如果要反覆使用相同的邏輯,那更應該這麼做

第五條:瞭解切割序列的方法

(1)不要寫多餘的程式碼:當start索引為0,或end索引為序列長度時,應將其省略a[:]

(2)切片操作不會計較start與end索引是否越界,者使得我們很容易就能從序列的前端或後端開始,對其進行範圍固定的切片操作,a[:20]或a[-20:]

(3)對list賦值的時候,如果使用切片操作,就會把原列表中處在相關範圍內的值替換成新值,即便它們的長度不同也依然可以替換

第六條:在單詞切片操作內,不要同時指導start、end和step

(1)這條的目的主要是怕程式碼難以閱讀,作者建議將其拆解為兩條賦值語句,一條做範圍切割,另一條做步進切割

(2)注意:使用[::-1]時會出現不符合預期的錯誤,看下面的例子

msg = '謝謝'
print('msg:',msg)
x = msg.encode('utf-8')
y = x.decode('utf-8')
print('y:',y)
z=x[::-1].decode('utf-8')
print('z:', z)

輸出:

第七條:用列表推導式來取代map和filter

(1)列表推導要比內建的map和filter函式清晰,因為它無需額外編寫lambda表示式

(2)字典與集合也支援推導表示式

第八條:不要使用含有兩個以上表達式的列表推導式

第九條:用生成器表示式來改寫資料量較大的列表推導式

(1)列表推導式的缺點

在推導過程中,對於輸入序列中的每個值來說,可能都要建立僅含一項元素的全新列表,當輸入的資料比較少時,不會出現問題,但如果輸入資料非常多,那麼可能會消耗大量記憶體,並導致程式崩潰,面對這種情況,python提供了生成器表示式,它是列表推導和生成器的一種泛化,生成器表示式在執行的時候,並不會把整個輸出序列呈現出來,而是會估值為迭代器。

把實現列表推導式所用的那種寫法放在一對園括號中,就構成了生成器表示式

numbers = [1,2,3,4,5,6,7,8]
li = (i for i in numbers)
print(li)

>>>> <generator object <genexpr> at 0x0000022E7E372228>

(2)串在一起的生成器表示式執行速度很快

第十條:儘量用enumerate取代range

(1)儘量使用enumerate來改寫那種將range與下表訪問結合的序列遍歷程式碼

(2)可以給enumerate提供第二個引數,以指定開始計數器時所用的值,預設為0

color = ['red','black','write','green']
#range方法
for i in range(len(color)):
    print(i,color[i])

#enumrate方法
for i,value in enumerate(color):
    print(i,value)

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

(1)內建的zip函式可以平行地遍歷多個迭代器

(2)Python3中的zip相當於生成器,會在遍歷過程中逐次產生元組,而python2中的zip則是直接把這些元組完全生成好,並一次性地返回整份列表、

(3)如果提供的迭代器長度不等,那麼zip就會自動提前終止

attr = ['name','age','sex']
values = ['zhangsan',18,'man']

people = zip(attr,values)
for p in people:
    print(p)

第12條:不要在for和while迴圈後面寫else塊

(1)python提供了一種很多程式語言都不支援的功能,那就是在迴圈內部的語句塊後面直接編寫else塊

for i in range(3):
    print('loop %d' %(i))
else:
    print('else block!')

上面的寫法很容易讓人產生誤解:如果迴圈沒有正常執行完,那就執行else,實際上剛好相反

(2)不要再迴圈後面使用else,因為這種寫法既不直觀,又容易讓人誤解

第13條:合理利用try/except/else/finally結構中的每個程式碼塊

try:
    #執行程式碼
except:
    #出現異常
else:
    #可以縮減try中程式碼,再沒有發生異常時執行
finally:
    #處理釋放操作

2. 函式

第14條:儘量用異常來表示特殊情況,而不要返回None

(1)用None這個返回值來表示特殊意義的函式,很容易使呼叫者犯錯,因為None和0及空字串之類的值,在表示式裡都會貝評估為False

(2)函式在遇到特殊情況時應該丟擲異常,而不是返回None,呼叫者看到該函式的文件中所描述的異常之後,應該會編寫相應的程式碼來處理它們

第15條:瞭解如何在閉包裡使用外圍作用域中的變數

(1)理解什麼是閉包

閉包是一種定義在某個作用域中的函式,這種函式引用了那個作用域中的變數

(2)表示式在引用變數時,python直譯器遍歷各作用域的順序:

a. 當前函式的作用域

b. 任何外圍作用域(例如:包含當前函式的其他函式)

c. 包含當前程式碼的那個模組的作用域(也叫全域性作用域)

d. 內建作用域(也即是包含len及str等函式的那個作用域)

e. 如果上賣弄這些地方都沒有定義過名稱相符的變數,那麼就丟擲NameError異常

(3)賦值操作時,python直譯器規則

給變數賦值時,如果當前作用域內已經定義了這個變數,那麼該變數就會具備新值,若當前作用域內沒有這個變數,python則會把這次賦值視為對該變數的定義

(4)nonlocal

nonlocal的意思:給相關變數賦值的時候,應該在上層作用域中查詢該變數,nomlocal的唯一限制在於,它不能延申到模組級別,這是為了防止它汙染全域性作用域

(5)global

global用來表示對該變數的賦值操作,將會直接修改模組作用域的那個變數

第16條:考慮用生成器來改寫直接返回列表的函式

參考第九條

第17條:在引數上面迭代時,要多加小心

(1)函式在輸入的引數上面多次迭代時要當心,如果引數是迭代物件,那麼可能會導致奇怪的行為並錯失某些值

看下面兩個例子:

例1:

def normalize(numbers):
    total = sum(numbers)
    print('total:',total)
    print('numbers:',numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result
    
numbers = [15,35,80]
print(normalize(numbers))

輸出:

例2:將numbers換成生成器

def fun():
    li = [15,35,80]
    for i in li:
        yield i
        
print(normalize(fun()))

輸出:

原因:迭代器只產生一輪結果,在丟擲過StopIteration異常的迭代器或生成器上面繼續迭代第二輪,是不會有結果的。

(2)python的迭代器協議,描述了容器和迭代器應該如何於iter和next內建函式、for迴圈及相關表示式互相配合

(3) 想判斷某個值是迭代器還是容器 ,可以拿該值為引數,兩次呼叫iter函式,若結果相同,則是迭代器,呼叫內建的next函式,即可令該迭代器前進一步

if iter(numbers) is iter(numbers):
    raise TypeError('Must supply a container')

第18條:用數量可變的位置引數減少視覺雜訊

(1)在def語句中使用*args,即可令函式接收數量可變的位置引數

(2)呼叫函式時,可以採用*操作符,把序列中的元素當成位置引數,傳給該函式

(3)對生成器使用*操作符,可能導致程式耗盡記憶體並崩潰,所以只有當我們能夠確定輸入的引數個數比較少時,才應該令函式接受*arg式的變長引數

(4) 在已經接收*args引數的函式上面繼續新增位置引數 ,可能會產生難以排查的錯誤

第19條:用關鍵字引數來表達可選的行為

(1)函式引數可以按位置或關鍵字來指定

(2)只使用位置引數來呼叫函式,可能會導致這些引數值的含義不夠明確,而關鍵字引數則能夠闡明每個引數的意圖

(3)該函式新增新的行為時,可以使用帶預設值的關鍵字引數,以便與原有的函式呼叫程式碼保持相容

(4) 可選的關鍵字引數 總是應該以關鍵字形式來指定,而不應該以位置引數來指定

第20條:用None和文件字串來描述具有動態預設值的引數

import datetime
import time
def log(msg,when=datetime.datetime.now()):
    print('%s:%s' %(when,msg))
    
log('hi,first')
time.sleep(1)
log('hi,second')

輸出:

兩次顯示的時間一樣,這是因為datetime.now()只執行了一次,也就是它只在函式定義的時候執行了一次,引數的預設值,會在每個模組載入進來的時候求出,而很多模組都在程式啟動時載入。我們可以將上面的函式改成:

import datetime
import time
def log(msg,when=None):
    """
    arg when:datetime of when the message occurred
    """
    
    if when is None:
        when=datetime.datetime.now()
    print('%s:%s' %(when,msg))
    
log('hi,first')
time.sleep(1)
log('hi,second')

輸出:

(1)引數的預設值,只會在程式載入模組並讀到本函式定義時評估一次,對於{}或[]等動態的值,這可能導致奇怪的行為

(2)對於以動態值作為實際預設值的關鍵字引數來說,應該把形式上的預設值寫為None,並在函式的文件字串裡面描述該預設值所對應的實際行為

第21條:用只能以關鍵字形式指定的引數來確保程式碼明確

(1)關鍵字引數能夠使函式呼叫的意圖更加明確

(2)對於各引數之間很容易混淆的函式,可以宣告只能以關鍵字形式指定的引數,以確保呼叫者必須通過關鍵字來指定它們。對於接收多個Boolean標誌的函式更應該這樣做

3. 類與繼承

第22條:儘量用輔助類來維護程式的狀態,而不要用字典或元組

作者的意思是:如果我們使用字典或元組儲存程式的某部分資訊,但隨著需求的不斷變化,需要逐漸的修改之前定義好的字典或元組結構,會出現多次的巢狀,過分膨脹會導致程式碼出現問題,而且難以理解。遇到這樣的情況,我們可以把巢狀結構重構為類。

(1)不要使用包含其他字典的字典,也不要使用過長的元組

(2)如果容器中包含簡單而又不可變的資料,那麼可以先使用namedtupe來表述,待稍後有需要時,再修改為完整的類

注意:namedtuple類無法指定各引數的預設值,對於可選屬性比較多的資料來說,namedtuple用起來不方便

(3)儲存內部狀態的字典如果變得比較複雜,那就應該把這些程式碼拆分為多個輔組類

第23條:簡單的介面應該接收函式,而不是類的例項

(1)對於連線各種python元件的簡單介面來說,通常應該給其直接傳入函式,而不是先定義某個類,然後再傳入該類的例項

(2)Python種的函式和方法可以像類那麼引用,因此,它們與其他型別的物件一樣,也能夠放在表示式裡面

(3)通過名為__call__的特殊方法,可以使類的例項能夠像普通的Python函式那樣得到呼叫

第24條:以@classmethod形式的多型去通用的構建物件

在python種,不僅物件支援多型,類也支援多型

(1)在Python程式種,每個類只能有一個構造器,也就是__init__方法

(2)通過@classmethod機制,可以用一種與構造器相仿的方式來構造類的物件

(3)通過類方法機制,我們能夠以更加通用的方式來構建並拼接具體的子類

下面以實現一套MapReduce流程計算檔案行數為例來說明:

(1)思路

(2)上程式碼

import threading
import os

class InputData:
    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()  
        
class worker:
    def __init__(self,input_data):
        self.input_data = input_data
        self.result = None
        
    def map(self):
        raise NotImplementedError
        
    def reduce(self):
        raise NotImplementedError
        
class LineCountWorker(worker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')
        
    def reduce(self,other):
        self.result += other.result
        
def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir,name))
        
def create_workers(input_list):
    workers = []
    for input_data in input_list:
        workers.append(LineCountWorker(input_data))
    return workers
    
def execute(workers):
    threads = [threading.Thread(target=w.map) for w in workers]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
        
    first,rest = workers[0],workers[1:]
    for worker in rest:
        first.reduce(worker)
    return first.result
    
def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)
    
if __name__ == "__main__":
    print(mapreduce('D:\mapreduce_test'))

MapReduce

上面的程式碼在拼接各種元件時顯得非常費力,下面重新使用@classmethod來改進下

import threading
import os

class InputData:
    def read(self):
        raise NotImplementedError
    
    @classmethod
    def generate_inputs(cls,data_dir):
        raise NotImplementedError

class PathInputData(InputData):
    def __init__(self,path):
        super().__init__()
        self.path = path
    
    def read(self):
        return open(self.path).read()  
        
    @classmethod
    def generate_inputs(cls,data_dir):
        for name in os.listdir(data_dir):
            yield cls(os.path.join(data_dir,name))
        
class worker:
    def __init__(self,input_data):
        self.input_data = input_data
        self.result = None
        
    def map(self):
        raise NotImplementedError
        
    def reduce(self):
        raise NotImplementedError
        
    @classmethod
    def create_workers(cls,input_list):
        workers = []
        for input_data in input_list:
            workers.append(cls(input_data))
        return workers
        
class LineCountWorker(worker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')
        
    def reduce(self,other):
        self.result += other.result
        
def execute(workers):
    threads = [threading.Thread(target=w.map) for w in workers]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
        
    first,rest = workers[0],workers[1:]
    for worker in rest:
        first.reduce(worker)
    return first.result
    
def mapreduce(data_dir):
    inputs = PathInputData.generate_inputs(data_dir)
    workers = LineCountWorker.create_workers(inputs)
    return execute(workers)
    
if __name__ == "__main__":
    print(mapreduce('D:\mapreduce_test'))

修改後的MapReduce

通過類方法實現多型機制,我們可以用更加通用的方式來構建並拼接具體的類

第25條:用super初始化父類

如果從python2開始詳細的介紹super使用方法需要很大的篇幅,這裡只介紹python3中的使用方法和MRO

(1)MRO即為方法解析順序,以標準的流程來安排超類之間的初始化順序,深度優先,從左至右,它也保證鑽石頂部那個公共基類的__init__方法只會執行一次

(2)python3中super的使用方法

python3提供了一種不帶引數的super呼叫方法,該方式的效果與用__class__和self來呼叫super相同

class A(Base):
    def __init__(self,value):
        super(__class__,self).__init__(value)
        
class A(Base):
    def __init__(self,value):
        super().__init__(value)

推薦使用上面兩種方法,python3可以在方法中通過__class__變數精確的引用當前類,而Python2中則沒有定義__class__方法

(3)總是應該使用內建的super函式來初始化父類

第26條:只在使用Mix-in元件製作工具類時進行多重繼承

python是面向物件的程式語言,它提供了一些內建的程式設計機制,使得開發者可以適當地實現多重繼承,但是,我們應該儘量避免多重繼承,若一定要使用,那就考慮編寫mix-in類,mix-in是一種小型的類,它只定義了其他類可能需要提供的一套附加方法,而不定義自己的 例項屬性,此外,它也不要求使用者呼叫自己的__init__函式

(1)能用mix-in元件實現的效果,就不要使用多重繼承來做

(2)將各功能實現為可插拔的mix-in元件,然後令相關的類繼承自己需要的那些元件,即可定製該類例項所具備的行為

(3)把簡單的行為封裝到mix-in元件裡,然後就可以用多個mix-in組合出複雜的行為了

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

python沒有從語法上嚴格保證private欄位的私密性,用簡單的話來說,我們都是成年人。

個人習慣:_XXX 單下劃代表protected;__XXX 雙下劃線開始的且不以_結尾表示private;__XXX__系統定義的屬性和方法

class People:
    __name="zhanglin"
    
    def __init__(self):
        self.__age = 16
    
print(People.__dict__)
p = People()
print(p.__dict__)

會發現__name和__age屬性名都發生了變化,都變成了(_類名+屬性名), 只有在__XXX這種命名方式下才會發生變化,所以以這種方式作為偽私有說明

(1)python編譯器無法嚴格保證private欄位的私密性

(2)不要盲目地將屬性設為private,而是應該從一開始就做好規劃,並允許子類更多地訪問超類內部的api

(3)應該更多的使用protected屬性,並在文件中把這些欄位的合理用法告訴子類的開發者,而不是試圖用private屬性來限制子類訪問這些欄位

(4)只有當子類不受自己控制時,才可以考慮用private屬性來避免名稱衝突

第28條:繼承collections.abc以實現自定義的容器型別

collections.abc模組定義了一系列抽象基類,它們提供了每一種容器型別所應具備的常用方法,大家可以自己參考原始碼

__all__ = ["Awaitable", "Coroutine",
           "AsyncIterable", "AsyncIterator", "AsyncGenerator",
           "Hashable", "Iterable", "Iterator", "Generator", "Reversible",
           "Sized", "Container", "Callable", "Collection",
           "Set", "MutableSet",
           "Mapping", "MutableMapping",
           "MappingView", "KeysView", "ItemsView", "ValuesView",
           "Sequence", "MutableSequence",
           "ByteString",
           ]

(1)如果定製的子類比較簡單,那就可以直接從Python的容器型別(如list、dict)中繼承

(2)想正確實現自定義的容器型別,可能需要編寫大量的特殊方法

(3)編寫自制的容器型別時,可以從collections.abc模組的抽象基類中繼承,那些基類能夠確保我們的子類具備適當的介面及行為

4. 元類及屬性

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

(1)編寫新類時,應該用簡單的public屬性來定義其介面,而不要手工實現set和get方法

(2)如果訪問物件的某個屬性,需要表現出特殊的行為,那就用@property來定義這種行為

比如下面的示例:成績必須在0-100範圍內

class Homework:
    def __init__(self):
        self.__grade = 0
        
    @property
    def grade(self):
        return self.__grade
        
    @grade.setter
    def grade(self,value):
        if not (0<=value<=100):
            raise ValueError('Grade must be between 0 and 100')
        self.__grade = value

(3)@property方法應該遵循最小驚訝原則,而不應該產生奇怪的副作用

(4)@property方法需要執行得迅速一些,緩慢或複雜的工作,應該放在普通的方法裡面

(5)@property的最大缺點在於和屬性相關的方法,只能在子類裡面共享,而與之無關的其他類都無法複用同一份實現程式碼

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

作者的意思是:當我們需要遷移屬性時(也就是對屬性的需求發生變化的時候),我們只需要給本類新增新的功能,原來的那些呼叫程式碼都不需要改變,它在持續完善介面的過程中是一種重要的緩衝方案

(1)@property可以為現有的例項屬性新增新的功能

(2)可以用@properpy來逐步完善資料模型

(3)如果@property用的太過頻繁,那就應該考慮徹底重構該類並修改相關的呼叫程式碼

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

首先對描述符進行說明,先看下面的例子:

class Grade:
    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:
    math_grade = Grade()
    chinese_grade = Grade()
    science_grade = Grade()

if __name__ == "__main__":
    exam = Exam()
    exam.math_grade = 99
    
    exam1 = Exam()
    exam1.math_grade = 75
    print('exam.math_grade:',exam.math_grade, 'is wrong')
    print('exam1.math_grade:',exam1.math_grade, 'is right')

輸出:

會發現在兩個Exam例項上面分別操作math_grade時,導致了錯誤的結果,出現這種情況的原因是因為 該math_grade屬性為Exam類的例項 ,為了解決這個問題,看下面的程式碼

class Grade:
    def __init__(self):
        self.__value = {}
        
    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return self.__value.get(instance,0)
    
    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self.__value[instance] = value
        
class Exam:
    math_grade = Grade()
    chinese_grade = Grade()
    science_grade = Grade()

if __name__ == "__main__":
    exam = Exam()
    exam.math_grade = 99
    exam1 = Exam()
    exam1.math_grade = 75
    print('exam.math_grade:',exam.math_grade, 'is wrong')
    print('exam1.math_grade:',exam1.math_grade, 'is right')

輸出:

上面這種實現方式很簡單,而且能夠正常運作,但它仍然有個問題,那就是會洩露記憶體,在程式的生命期內,對於傳給__set__方法的每個Exam例項來說,__values字典都會儲存指向該例項的一份引用,者就導致例項的引用計數無法降為0,從而使垃圾收集器無法將其收回。使用python的內建weakref模組,可解決上述問題。

class Grade:
    def __init(self):
        self.__value = weakref.WeakKeyDictionary()
        

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

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

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

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

如果某個類定義了__getattr__,同時系統在該類物件的例項字典中又找不到待查詢的屬性,那麼就會呼叫這個方法

惰性訪問的概念:初次執行__getattr__的時候進行一些操作,把相關的屬性載入進來,以後再訪問該屬性時,只需從現有的結果中獲取即可

程式每次訪問物件的屬性時,Python系統都會呼叫__getattribute__,即使屬性字典裡面已經有了該屬性,也以讓會觸發__getattribute__方法

(1)通過__getattr__和__setattr__,我們可以用惰性的方式來載入並儲存物件的屬性

(2)要理解__getattr__和__getattribute__的區別:前者只會在待訪問的屬性缺失時觸發,,而後者則會在每次訪問屬性時觸發

(3)如果要在__getattribute__和__setattr__方法中訪問例項屬性,那麼應該直接通過super()來做,以避免無限遞迴

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

元類最簡單的一種用途,就是驗證某個類定義的是否正確,構建複雜的類體系時,我們可能需要確保類的風格協調一致,確保某些方法得到了覆寫,或是確保類屬性之間具備某些嚴格的關係。

下例判斷類屬性中是否含有name屬性:

#驗證某個類的定義是否正確
class Meta(type):
    def __new__(meta,name,bases,class_dict):
        print('class_dict:',class_dict)
        if not class_dict.get('name',None):   #判斷類屬性中是否含有name屬性
            raise AttributeError('must has name attribute')
        return type.__new__(meta,name,bases,class_dict)
            
class A(metaclass=Meta):
    def __init__(self):
        self.chinese_grade = 90
        self.math_grade = 99
        
if __name__ == '__main__':
    a = A()

輸出:

(1)通過元類,我們可以在生成子類物件之前,先驗證子類的定義是否合乎規範

(2)python系統把子類的整個class語句體處理完畢之後,就會呼叫其元類的__new__方法

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

元類還有一個用途就是在程式中自動註冊型別,對於需要反向查詢(reverse lookup)的場合,這種註冊操作很有用

看下面的例子:對物件進行序列化和反序列化

import json

register = {}
class Meta(type):
    def __new__(meta,name,bases,attr_dic):
        cls = type.__new__(meta,name,bases,attr_dic)
        print('create class in Meta:', cls)
        register[cls.__name__] = cls
        return cls
        
class Serializable(metaclass=Meta):
    def __init__(self,*args):
        self.args = args
        
    def serialize(self):
        return json.dumps({'class':self.__class__.__name__, 'args':self.args})
        
    def deserilize(self,json_data):
        json_dict = json.loads(json_data)
        classname = json_dict['class']
        args = json_dict['args']
        return register[classname](*args)
        
class Point2D(Serializable):
    def __init__(self,x,y):
        super().__init__(x,y)
        self.x = x
        self.y = y
        
    def add(self):
        return self.x + self.y
        
if __name__ == "__main__":
    p = Point2D(2,5)
    data = p.serialize()
    print('serialize_data:',data)
    new_point2d = p.deserilize(data)
    print('new_point2d:',new_point2d)
    print(new_point2d.add())

輸出:

(1)通過元類來實現類的註冊,可以確保所有子類就都不會洩露,從而避免後續的錯誤

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

(1)藉助元類,我們可以在某個類完全定義好之前,率先修改該類的屬性

(2)描述符與元類能夠有效的組合起來,以便對某種行為做出修飾,或在程式執行時探查相關資訊

(3)如果把元類與描述符相結合,那就可以在不使用weakref模組的前提下避免記憶體洩漏

5. 併發與並行

併發和並行的關鍵區別在於能不能提速,若是並行,則總任務的執行時間會減半,若是併發,那麼即使可以看似平行的方式分別執行多條路徑,依然不會使總任務的執行速度得到提升,用Python語言編寫併發程式,是比較容易的,通過系統呼叫、子程序和C語言擴充套件等機制,也可以用Python平行地處理一些事務,但是,要想使併發式的python程式碼以真正平行的方式來執行,卻相當困難。

可以先閱讀我之前的部落格,相信會有幫組: python究竟要不要使用多執行緒

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

在多年的發展過程中,Python演化出了多種執行子程序的方式,其中包括popen、popen2和os.exec*等,然而,對於至今的Python來說,最好且最簡單的子程序管理模組,應該是內建的subprocess模組

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

(1)因為受全域性解釋鎖(GIL)的限制,所以多條Python執行緒不能在多個CPU核心上面平行地執行位元組碼

(2)儘管受制於GIL,但是python的多執行緒功能依然很有用,它可以輕鬆地模擬出同一時刻執行多項任務的效果

(3)通過python執行緒,我們可以平行地執行多個系統呼叫,這使得程式能夠在執行阻塞式I/O操作的同時,執行一些運算操作

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

class LockingCounter:
    def __init__(self):
        self.lock = threading.Lock()
        self.count = 0
        
    def increment(self, offset):
        with self.lock:
            self.count += offset

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

作者舉了一個照片處理系統的例子:

需求:該系統從數碼相機裡面持續獲取照片、調整其尺寸,並將其新增到網路相簿中。

實現:使用三階段的管線實現,需要4個自定義的deque訊息佇列,第一階段獲取新照片,第二階段把下載好的照片傳給縮放函式,第三階段把縮放後的照片交給上傳函式

問題:該程式雖然可以正常執行,但是每個階段的工作函式都會有差別,這使得前一階段可能會拖慢後一階段的進度,從而令整條管線遲滯,後一階段會在其迴圈語句中,反覆查詢輸入佇列,以求獲取新的任務,而任務卻遲遲未到達,這將令後一階段陷入飢餓,會白白浪費CPU時間,效率特低

內建的queue模組的Queue類可以解決上述問題,因為其get方法會持續阻塞,直到有新的資料加入

import threading
from queue import Queue

class ClosableQueue(Queue):
    SENTINEL = object()
    
    def close(self):
        self.put(SENTINEL)
        
    def __iter__(self):
        while True:
            item = self.get()
            try:
                if item is self.SENTINEL:
                    return 
                yield item
            finally:
                self.task_done()
                
class StoppabelWoker(threading.Thread):
    def __init__(self,func,in_queue,out_queue):
        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)

(1)管線是一種優秀的任務處理方式,它可以把處理流程劃分未若干個階段,並使用多條python執行緒來同時執行這些任務

(2)構建併發式的管線時,要注意許多問題,其中包括:如何防止某個階段陷入持續等待的狀態之中,如何停止工作執行緒,以及如何防止記憶體膨脹等

(3)Queue類所提供的機制,可以cedilla解決上述問題,它具備阻塞式的佇列操作,能夠指定緩衝區的尺寸,而且還支援join方法,這使得開發者可以構建出健壯的管線

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

(1)協程提供了一種有效的方式,令程式看上去好像能夠同時執行大量函式

(2)對於生成器內的yield表示式來說,外部程式碼通過send方法傳給生成器的那個值就是該表示式所要具備的值

(3)協程是一種強大的工具,它可以把程式的核心邏輯,與程式同外部環境互動時所使用的程式碼相隔離

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

參考之前的部落格: 網路爬蟲必備知識之concurrent.futures庫

6. 內建模組

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

為了維護函式的介面,修飾之後的函式,必須保留原函式的某些標準Python屬性,例如__name__和__module__,這個時候我們需要使用functools.wraps來確保修飾後函式具備正確的行為

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

(1)可以用with語句來改寫try/finally塊中的邏輯,以提升複用程度,並使程式碼更加整潔

import threading

lock = threading.Lock()
lock.acquier()
try:
    print("lock is held")
finally:
    lock.release()

可以直接使用下面的語法:

import threading

lock = threading.Lock()
with lock:
    print("lock is held")

(2)內建的contextlib模組提供了名叫為contextmanager的修飾器,開發者只需要用它來修飾自己的函式,即可令該函式支援with語句

from contextlib import contextmanager

@contextmanager
def file_open(path):
    ''' file open test'''
    try:
        fp = open(path,"wb")
        yield fp
    except OSError:
        print("We had an error!")
    finally:
        print("Closing file")
        fp.close()

if __name__ == "__main__": 
    with file_open("contextlibtest.txt") as fp:
        fp.write("Testing context managers".encode("utf-8"))

(3)情景管理器可以通過yield語句向with語句返回一個值,此值會賦給由as關鍵字所指定的變數

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

(1)內建的pickle模組,只適合用來彼此信任的程式之間,對相關物件執行序列化和反序列化操作

(2)如果用法比較複雜,那麼pickle模組的功能可能就會出現問題,我們可以用內建的copyreg模組和pickle結合起來使用,以便為舊資料新增缺失的屬性值、進行類的版本管理、並給序列化之後的資料提供固定的引入路徑

第45條:應該用datetime模組來處理本地時間,而不是time模組

(1)不要用time模組在不同時區之間進行轉換

(2)如果要在不同時區之間,可靠地執行轉換操作,那就應該把內建的datetime模組與開發者社群提供的pytz模組打起來使用

(3)開發者總是應該先把時間表示為UTC格式,然後對其執行各種轉換操作,最後再把它轉回本地時間

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

(1)雙向佇列 collections.deque

(2)有序字典 dollections.OrderDict

(3)帶有預設值的有序字典 collections.defaultdict

(4)堆佇列(優先順序佇列)heapq.heap

(5)二分查詢 bisect模組中的bisect_left函式等提供了高效的二分折半搜尋演算法

(6)與迭代器有關的工具 itertools模組

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

(1)decimal模組中的Decimal類預設提供28個小數位,以進行定點數字運算,還可以按照開發射所要求的精度及四捨五入

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

7. 協作開發

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

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

(1)只要把__init__.py檔案放入含有其他原始檔的目錄裡,就可以將該目錄定義為包,目錄中的檔案,都將成為包的子模組,該包的目錄下面,也可以含有其他的包

(2)把外界可見的名稱,列在名為__all__的特殊屬性裡,即可為包提供一套明確的API

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

意思就是單獨用個模組提供各種異常API

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

(1)調整引入順序

(2)先引入、再配置、最後執行

只在模組中給出函式、類和常量的定義,而不要在引入的時候真正去執行那些函式

(3)動態引入:在函式或方法內部使用import語句

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

參考之前的部落格: Python之用虛擬環境隔離專案,並重建依賴關係

8. 部署

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

(1)可以根據外部條件來決定模組的內容,例如,通過sys和os模組來查詢宿主作業系統的特性,並以此來定義本模組中的相關結構

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

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

這個在後面會單獨寫篇部落格對unittest單元測試模組進行詳細說明

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

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

(1)優化python程式之前,一定要先分析其效能,因為python程式的效能瓶頸通常很難直接觀察出來

(2)做效能分析時,應該使用cProfile模組,而不要使用profile模組,因為前者能夠給出更為精確的效能分析資料

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

在Python的預設實現中,也就是Cpython中,記憶體管理是通過引用計數來處理的,另外,Cpython還內建了迴圈檢測器,使得垃圾回收機制能夠把那些自我引用的物件清除掉

(1)使用內建的gc模組進行查詢,列出垃圾收集器當前所知道的每個物件,該方法相當笨拙

(2)python3.4提供了內建模組tracemalloc可以打印出Python系統在執行每一個分配記憶體操作時所具備的完整堆疊資訊

文章到這裡就全部結束了,感謝您這麼有耐心的閱讀!