這個週末斷斷續續的閱讀完了《Effective Python之編寫高質量Python代碼的59個有效方法》,感覺還不錯,具有很大的指導價值。 下面將以最簡單的方式記錄這59條建議,並在大部分建議後面加上了說明和示例,文章篇幅大,請您提前備好瓜子和啤酒!
另外文末附贈415集全套python視頻教程,全部分享給大家!
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]時會出現不符合預期的錯誤,看下面的例子
<code>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)/<code>
輸出:
第七條:用列表推導式來取代map和filter
(1)列表推導要比內置的map和filter函數清晰,因為它無需額外編寫lambda表達式
(2)字典與集合也支持推導表達式
第八條:不要使用含有兩個以上表達式的列表推導式
第九條:用生成器表達式來改寫數據量較大的列表推導式
(1)列表推導式的缺點
在推導過程中,對於輸入序列中的每個值來說,可能都要創建僅含一項元素的全新列表,當輸入的數據比較少時,不會出現問題,但如果輸入數據非常多,那麼可能會消耗大量內存,並導致程序崩潰,面對這種情況,python提供了生成器表達式,它是列表推導和生成器的一種泛化,生成器表達式在運行的時候,並不會把整個輸出序列呈現出來,而是會估值為迭代器。
把實現列表推導式所用的那種寫法放在一對園括號中,就構成了生成器表達式
<code>numbers = [1,2,3,4,5,6,7,8]li = (i for i in numbers)print(li)/<code>
>>>> <generator> at 0x0000022E7E372228>/<generator>
(2)串在一起的生成器表達式執行速度很快
第十條:儘量用enumerate取代range
(1)儘量使用enumerate來改寫那種將range與下表訪問結合的序列遍歷代碼
(2)可以給enumerate提供第二個參數,以指定開始計數器時所用的值,默認為0
<code>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)/<code>
第11條:用zip函數同時遍歷兩個迭代器
(1)內置的zip函數可以平行地遍歷多個迭代器
(2)Python3中的zip相當於生成器,會在遍歷過程中逐次產生元組,而python2中的zip則是直接把這些元組完全生成好,並一次性地返回整份列表、
(3)如果提供的迭代器長度不等,那麼zip就會自動提前終止
<code>attr = ['name','age','sex']values = ['zhangsan',18,'man']people = zip(attr,values)for p in people: print(p)/<code>
第12條:不要在for和while循環後面寫else塊
(1)python提供了一種很多編程語言都不支持的功能,那就是在循環內部的語句塊後面直接編寫else塊
<code>for i in range(3): print('loop %d' %(i))else: print('else block!')/<code>
上面的寫法很容易讓人產生誤解:如果循環沒有正常執行完,那就執行else,實際上剛好相反
(2)不要再循環後面使用else,因為這種寫法既不直觀,又容易讓人誤解
第13條:合理利用try/except/else/finally結構中的每個代碼塊
<code>try: #執行代碼except: #出現異常else: #可以縮減try中代碼,再沒有發生異常時執行finally: #處理釋放操作/<code>
2. 函數
第14條:儘量用異常來表示特殊情況,而不要返回None
(1)用None這個返回值來表示特殊意義的函數,很容易使調用者犯錯,因為None和0及空字符串之類的值,在表達式裡都會貝評估為False
(2)函數在遇到特殊情況時應該拋出異常,而不是返回None,調用者看到該函數的文檔中所描述的異常之後,應該會編寫相應的代碼來處理它們
(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:
<code>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))/<code>
輸出:
例2:將numbers換成生成器
<code>def fun(): li = [15,35,80] for i in li: yield i print(normalize(fun()))/<code>
輸出:
原因:迭代器只產生一輪結果,在拋出過StopIteration異常的迭代器或生成器上面繼續迭代第二輪,是不會有結果的。
(2)python的迭代器協議,描述了容器和迭代器應該如何於iter和next內置函數、for循環及相關表達式互相配合
(3) 想判斷某個值是迭代器還是容器 ,可以拿該值為參數,兩次調用iter函數,若結果相同,則是迭代器,調用內置的next函數,即可令該迭代器前進一步
<code>if iter(numbers) is iter(numbers): raise TypeError('Must supply a container')/<code>
第18條:用數量可變的位置參數減少視覺雜訊
(1)在def語句中使用*args,即可令函數接收數量可變的位置參數
(2)調用函數時,可以採用*操作符,把序列中的元素當成位置參數,傳給該函數
(3)對生成器使用*操作符,可能導致程序耗盡內存並崩潰,所以只有當我們能夠確定輸入的參數個數比較少時,才應該令函數接受*arg式的變長參數
(4) 在已經接收*args參數的函數上面繼續添加位置參數 ,可能會產生難以排查的錯誤
第19條:用關鍵字參數來表達可選的行為
(1)函數參數可以按位置或關鍵字來指定
(2)只使用位置參數來調用函數,可能會導致這些參數值的含義不夠明確,而關鍵字參數則能夠闡明每個參數的意圖
(3)該函數添加新的行為時,可以使用帶默認值的關鍵字參數,以便與原有的函數調用代碼保持兼容
(4) 可選的關鍵字參數 總是應該以關鍵字形式來指定,而不應該以位置參數來指定
第20條:用None和文檔字符串來描述具有動態默認值的參數
<code>import datetimeimport timedef log(msg,when=datetime.datetime.now()): print('%s:%s' %(when,msg)) log('hi,first')time.sleep(1)log('hi,second')/<code>
輸出:
兩次顯示的時間一樣,這是因為datetime.now()只執行了一次,也就是它只在函數定義的時候執行了一次,參數的默認值,會在每個模塊加載進來的時候求出,而很多模塊都在程序啟動時加載。我們可以將上面的函數改成:
<code>import datetimeimport timedef 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')/<code>
輸出:
(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)上代碼
<code>import threadingimport osclass InputData: def read(self): raise NotImplementedErrorclass 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/<code>
上面的代碼在拼接各種組件時顯得非常費力,下面重新使用@classmethod來改進下
<code>import threadingimport osclass InputData: def read(self): raise NotImplementedError @classmethod def generate_inputs(cls,data_dir): raise NotImplementedErrorclass 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/<code>
通過類方法實現多態機制,我們可以用更加通用的方式來構建並拼接具體的類
第25條:用super初始化父類
如果從python2開始詳細的介紹super使用方法需要很大的篇幅,這裡只介紹python3中的使用方法和MRO
(1)MRO即為方法解析順序,以標準的流程來安排超類之間的初始化順序,深度優先,從左至右,它也保證鑽石頂部那個公共基類的__init__方法只會運行一次
(2)python3中super的使用方法
python3提供了一種不帶參數的super調用方法,該方式的效果與用__class__和self來調用super相同
<code>class A(Base): def __init__(self,value): super(__class__,self).__init__(value) class A(Base): def __init__(self,value): super().__init__(value)/<code>
推薦使用上面兩種方法,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__系統定義的屬性和方法
<code>class People: __name="zhanglin" def __init__(self): self.__age = 16 print(People.__dict__)p = People()print(p.__dict__)/<code>
會發現__name和__age屬性名都發生了變化,都變成了(_類名+屬性名), 只有在__XXX這種命名方式下才會發生變化,所以以這種方式作為偽私有說明
(1)python編譯器無法嚴格保證private字段的私密性
(2)不要盲目地將屬性設為private,而是應該從一開始就做好規劃,並允許子類更多地訪問超類內部的api
(3)應該更多的使用protected屬性,並在文檔中把這些字段的合理用法告訴子類的開發者,而不是試圖用private屬性來限制子類訪問這些字段
(4)只有當子類不受自己控制時,才可以考慮用private屬性來避免名稱衝突
第28條:繼承collections.abc以實現自定義的容器類型
collections.abc模塊定義了一系列抽象基類,它們提供了每一種容器類型所應具備的常用方法,大家可以自己參考源碼
<code>__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", ]/<code>
(1)如果定製的子類比較簡單,那就可以直接從Python的容器類型(如list、dict)中繼承
(2)想正確實現自定義的容器類型,可能需要編寫大量的特殊方法
(3)編寫自制的容器類型時,可以從collections.abc模塊的抽象基類中繼承,那些基類能夠確保我們的子類具備適當的接口及行為
4. 元類及屬性
第29條:用純屬性取代get和set方法
(1)編寫新類時,應該用簡單的public屬性來定義其接口,而不要手工實現set和get方法
(2)如果訪問對象的某個屬性,需要表現出特殊的行為,那就用@property來定義這種行為
比如下面的示例:成績必須在0-100範圍內
<code>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/<code>
(3)@property方法應該遵循最小驚訝原則,而不應該產生奇怪的副作用
(4)@property方法需要執行得迅速一些,緩慢或複雜的工作,應該放在普通的方法裡面
(5)@property的最大缺點在於和屬性相關的方法,只能在子類裡面共享,而與之無關的其他類都無法複用同一份實現代碼
第30條:考慮用@property來代替屬性重構
作者的意思是:當我們需要遷移屬性時(也就是對屬性的需求發生變化的時候),我們只需要給本類添加新的功能,原來的那些調用代碼都不需要改變,它在持續完善接口的過程中是一種重要的緩衝方案
(1)@property可以為現有的實例屬性添加新的功能
(2)可以用@properpy來逐步完善數據模型
(3)如果@property用的太過頻繁,那就應該考慮徹底重構該類並修改相關的調用代碼
第31條:用描述符來改寫需要複用的@property方法
首先對描述符進行說明,先看下面的例子:
<code>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')/<code>
輸出:
會發現在兩個Exam實例上面分別操作math_grade時,導致了錯誤的結果,出現這種情況的原因是因為 該math_grade屬性為Exam類的實例 ,為了解決這個問題,看下面的代碼
<code>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')/<code>
輸出:
上面這種實現方式很簡單,而且能夠正常運作,但它仍然有個問題,那就是會洩露內存,在程序的生命期內,對於傳給__set__方法的每個Exam實例來說,__values字典都會保存指向該實例的一份引用,者就導致實例的引用計數無法降為0,從而使垃圾收集器無法將其收回。使用python的內置weakref模塊,可解決上述問題。
<code>class Grade: def __init(self): self.__value = weakref.WeakKeyDictionary() /<code>
(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屬性:
<code>#驗證某個類的定義是否正確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()/<code>
輸出:
(1)通過元類,我們可以在生成子類對象之前,先驗證子類的定義是否合乎規範
(2)python系統把子類的整個class語句體處理完畢之後,就會調用其元類的__new__方法
第34條:用元類來註冊子類
元類還有一個用途就是在程序中自動註冊類型,對於需要反向查找(reverse lookup)的場合,這種註冊操作很有用
看下面的例子:對對象進行序列化和反序列化
<code>import jsonregister = {}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())/<code>
輸出:
(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來防止數據競爭
<code>class LockingCounter: def __init__(self): self.lock = threading.Lock() self.count = 0 def increment(self, offset): with self.lock: self.count += offset/<code>
第39條:用Queue來協調各線程之間的工作
作者舉了一個照片處理系統的例子:
需求:該系統從數碼相機裡面持續獲取照片、調整其尺寸,並將其添加到網絡相冊中。
實現:使用三階段的管線實現,需要4個自定義的deque消息隊列,第一階段獲取新照片,第二階段把下載好的照片傳給縮放函數,第三階段把縮放後的照片交給上傳函數
問題:該程序雖然可以正常運行,但是每個階段的工作函數都會有差別,這使得前一階段可能會拖慢後一階段的進度,從而令整條管線遲滯,後一階段會在其循環語句中,反覆查詢輸入隊列,以求獲取新的任務,而任務卻遲遲未到達,這將令後一階段陷入飢餓,會白白浪費CPU時間,效率特低
內置的queue模塊的Queue類可以解決上述問題,因為其get方法會持續阻塞,直到有新的數據加入
<code>import threadingfrom queue import Queueclass 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)/<code>
(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塊中的邏輯,以提升複用程度,並使代碼更加整潔
<code>import threadinglock = threading.Lock()lock.acquier()try: print("lock is held")finally: lock.release()/<code>
可以直接使用下面的語法:
<code>import threadinglock = threading.Lock()with lock: print("lock is held")/<code>
(2)內置的contextlib模塊提供了名叫為contextmanager的修飾器,開發者只需要用它來修飾自己的函數,即可令該函數支持with語句
<code>from contextlib import contextmanager@contextmanagerdef 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"))/<code>
(3)情景管理器可以通過yield語句向with語句返回一個值,此值會賦給由as關鍵字所指定的變量
(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系統在執行每一個分配內存操作時所具備的完整堆棧信息
文章到這裡就全部結束了,感謝您這麼有耐心的閱讀!
最後,小編分享一波2019最新的python全套教程最後小編為大家準備了6月份新出的python自學視頻教程,共計415集,免費分享給大家!私信小編“學習”即可獲取
2019Python自學教程全新升級為《Python+數據分析+機器學習》,九大階段能力逐級提升,打造技能更全面的全棧工程師。
內容太多,小編就不給大家一一截圖了,私信小編領取吧!