60個神級技巧,幫你寫出高質量的python代碼

60個神級技巧,幫你寫出高質量的python代碼

這個週末斷斷續續的閱讀完了《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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

第七條:用列表推導式來取代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>
60個神級技巧,幫你寫出高質量的python代碼

上面的寫法很容易讓人產生誤解:如果循環沒有正常執行完,那就執行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,調用者看到該函數的文檔中所描述的異常之後,應該會編寫相應的代碼來處理它們

第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:

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

輸出:

60個神級技巧,幫你寫出高質量的python代碼

例2:將numbers換成生成器

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

輸出:

60個神級技巧,幫你寫出高質量的python代碼

原因:迭代器只產生一輪結果,在拋出過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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

兩次顯示的時間一樣,這是因為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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

(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)思路

60個神級技巧,幫你寫出高質量的python代碼

(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>
60個神級技巧,幫你寫出高質量的python代碼

會發現__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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

會發現在兩個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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

上面這種實現方式很簡單,而且能夠正常運作,但它仍然有個問題,那就是會洩露內存,在程序的生命期內,對於傳給__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>

輸出:

60個神級技巧,幫你寫出高質量的python代碼

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

輸出:

60個神級技巧,幫你寫出高質量的python代碼

(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關鍵字所指定的變量

第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系統在執行每一個分配內存操作時所具備的完整堆棧信息

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

最後,小編分享一波2019最新的python全套教程最後小編為大家準備了6月份新出的python自學視頻教程,共計415集,免費分享給大家!私信小編“學習”即可獲取

2019Python自學教程全新升級為《Python+數據分析+機器學習》,九大階段能力逐級提升,打造技能更全面的全棧工程師。


60個神級技巧,幫你寫出高質量的python代碼


60個神級技巧,幫你寫出高質量的python代碼


60個神級技巧,幫你寫出高質量的python代碼




60個神級技巧,幫你寫出高質量的python代碼


60個神級技巧,幫你寫出高質量的python代碼


60個神級技巧,幫你寫出高質量的python代碼


60個神級技巧,幫你寫出高質量的python代碼


內容太多,小編就不給大家一一截圖了,私信小編領取吧!

python學習資料獲取方式

  1. 右上角點擊關注
  2. 評論區任意評論或者轉發一下
  3. 做完1、2步,私信回覆“資料”
  4. 私信不要多字,不要少字,不要錯字,私信方法:點擊我頭像,進入主頁面,右上角有私信功能,在關注的上方位置。



分享到:


相關文章: