在微信公眾號「極客起源」中輸入595586,可學習全部的《Python高效編程之88條軍規》系列文章。用程式語言寫代碼是自由的,編譯器不會強制你使用特定的格式編寫程序(只要符合語法,編譯器才不管你呢!)。所以很多程式設計師就會將Python當做自己熟悉的Java、C++等語言來用。不過這些編碼方式真的是最好的選擇嗎?本系列文章將為你揭秘88種在編寫Python代碼中的規則,這些規則將會讓你Python程序更加健壯,運行效率更高。Python的PEP 8是Python官方提供了關於如何格式化Python代碼的樣式指南。儘管可以用任何有效的方式編寫Python代碼,但是,使用一致的樣式會使你的代碼更易於訪問和閱讀,以及與其他Python程式設計師使用同一種樣式有助於項目上的分工協作。即使你是唯一會閱讀代碼的人,遵循樣式指南也可以讓你的代碼更容易維護,並可以避免許多常見錯誤。關於PEP 8的詳細內容,讀者可以查看下面的頁面:https://www.python.org/dev/peps/pep-0008/在Python語言中,空格是有實際意義的。Python程式設計師應該更關注空格的用法,下面是與空格相關的一些建議(並不一定要遵守,但按照這個規範,會讓你的Python程序看著更舒服):(2)儘管縮進可以使用任意多個空格,但建議統一使用4個空格進行縮進;(3)每行不應該有過多的字符,建議最多不要超過79的字符;(4)如果每行的字符過多(超過79個),應該折到下一行,而且應該在當前縮進的基礎上再使用4個空格進行縮進,如下圖所示:
(5)在文件中,如果函數和類相鄰,建議使用兩個空行將他們分開,這樣會讓代碼一目了然;(7)在字典中,不要在key和冒號(:)之間放置空格,如果對應的值與key和冒號在同一行,應該在值前面放置一個空格;(8)在變量賦值時,等號(=)前面和後面應該有一個空格;(9)對於類型注釋(type annotations),要確保變量和冒號直接沒有空格,而且要在類型信息前面使用一個空格,如下圖所示:
PEP 8程序的不同部分採用統一的命名風格。因為擁有統一風格的命名,會讓代碼更容易閱讀,下面是一些推薦的命名規則:(1)函數、變量和屬性應該使用小寫字母加下劃線(_)的風格,例如,get_name,product_id等;(2)被保護的(protected)的實例屬性應該在名字前面加一個下劃線,例如,_name,_product_id等;(3)私有(private)實例屬性應該在名字前面加兩個下劃線(__),例如,__name,__product_id等;(4)類名應該使用大駝峰格式,也就是每一個單詞首字母都要大寫,例如,MyClass,Test,Product等;(5)模塊層常量的名字所有的字母都應該大寫,如果包含多個單詞,中間用下劃線分隔,例如,PRODUCT_ID,OS_PATH等;(6)類中的實例方法的第1個參數應該使用self(儘管可以使用任意參數名,但推薦使用self),該參數引用了對象本身;(7)類方法的第1個參數應該使用cls(儘管可以使用任意參數名,但推薦使用cls),該參數引用了類的本身;Python禪宗指出:「應該有一種(最好只有一種)明顯的方式來完成你的工作。」。PEP 8正常嘗試按這個規則確定表達式和語句的編寫風格。(1)使用內聯求反(if a is not b)代替對正表達式的求反(if not a is b);(2)如果要判斷序列(字符串、列表、字典等)是否為空(是否有元素),並不建議通過序列長度是否為0來判斷(if len(somelist) == 0),而要直接使用not進行判斷,例如,if not somelist。如果somelist是空串或空序列,那麼not somelist就為True,當然,如果somelist不為空,那麼somelist就被認為是True;(3)儘量避免單行的if、for和while語句,除非是複合語句,否則為了清晰起見,應該將它們分布在多行;(4)如果表達式過長,建議將這樣的表達式分布在多行,這樣更容易閱讀。但注意要在每行結尾加連接符,並且從第2行開始在第1行的基礎上再往後縮進4個空格;(1)將import語句(包括from x import y和import x語句)放在文件的最頂端;(2)如果在import語句中涉及到模塊名,應該使用絕對的模塊名,而不要使用相對的模塊名。例如,為了從bar包導入foo模塊,應該使用from bar import foo,而不要使用Import foo;(3)如果必須要使用相對的模塊名,應該顯式使用from . import foo形式;軍規2:了解字節序列(bytes)和字符串(str)的差異在Python語言中,有兩個數據類型可以表示字符序列:字節序列和字符串。其中字節序列中包含了原始的,8位無符號的值,通常以ASCII編碼形式顯示:如果用字節序列表示字符序列,應該以b開頭,代碼如下:
a = b'h\x65llo'print(list(a))print(a)[104, 101, 108, 108, 111]b'hello'字符串的實例包含了Unicode編碼,這些編碼表示人類語言的文本字符:a = 'a\u0300 propos'print(list(a))print(a)['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']à propos值得注意的是,字符串並不包含與之關聯的二進位編碼,而字節序列也不包含與之關聯的文本編碼。為了將文本編碼數據轉換為二進位數據,必須調用字符串的encode方法。為了將二進位數據轉換為文本編碼數據,必須調用字節序列的decode方法。我們可以顯式地指定這些方法的編碼格式,或者接受這些方法的默認編碼格式。默認編碼格式通常是UTF-8,不過也並不是所有方法的默認編碼格式都是UTF-8,具體情況請看下面的內容。在編寫Python程序時,在例接口最遠的邊界(也就是最初接觸Unicode數據的地方)進行Unicode數據的編碼和解碼非常重要。這種方法通常被稱為Unicode三明治。程序的核心應使用包含Unicode數據的str類型,並且不應對字符編碼做任何假設。這種方法使你可以非常容易接受其他文本編碼(例如Latin-1,Shift JIS和Big5),同時嚴格限制輸出文本編碼(理想情況下為UTF-8)。字符類型之間的分拆將導致Python代碼中出現兩種常見情況:(1)操作的是包含UTF-8編碼(或其他編碼)的8位字節序列;(2)操作的是沒有特定編碼的Unicode字符串;def to_str(bytes_or_str): if isinstance(bytes_or_str, bytes): value = bytes_or_str.decode('utf-8') else: value = bytes_or_str return value print(repr(to_str(b'hello')))print(repr(to_str('world')))def to_bytes(bytes_or_str): if isinstance(bytes_or_str, str): value = bytes_or_str.encode('utf-8') else: value = bytes_or_str return value print(repr(to_bytes(b'hello')))print(repr(to_bytes('world')))在Python中處理原始8位值和Unicode字符串時,有兩個大陷阱。第一個問題是字節和字符串的工作方式看似相同,但是它們的實例彼此並不兼容,因此你必須仔細考慮要傳遞的字符序列的類型。字節序列與字符串都支持加號(+)運算,也就是說,可以用加號分別將字節序列和字符串連接起來,看下面的代碼:print(b'hello ' + b' world')print('hello ' + 'world')b'hello world'hello world但是不能將字節序列和字符串相加,例如,下面的代碼會拋出異常:print(b'hello ' + 'world')Traceback (most recent call last): File "/python/bytes_str.py", line 36, in <module> print(b'hello ' + 'world')TypeError: can't concat str to bytesprint('hello ' + b'world')Traceback (most recent call last): File "/python/bytes_str.py", line 37, in <module> print('hello ' + b'world') TypeError: can only concatenate str (not "bytes") to str如果兩側的操作數都是字節序列或字符串,那麼也可以用於邏輯比較(<、<=、>、>=等運算符)。print('hello' > 'world')print(b'hello' < b'world')與加號類似,字符串與字節序列不能直接比較,如下面的代碼會拋出異常:print(b'hello' > 'world')Traceback (most recent call last): File "/python/bytes_str.py", line 41, in <module> print(b'hello' > 'world')TypeError: '>' not supported between instances of 'bytes' and 'str'與=、<、<=、>=、>=這些運算符不同,字節序列和字符串可以直接使用「==」判定是否相等。不過這是個陷阱,就算字節序列和字符串表面上每一個字符都是相同的,返回的結果仍然是False。print(b'hello' == 'hello')print(b'hello %s' % b'world')print('hello %s' % 'world')b'hello world'hello world但是不能傳遞字符串到字節序列中(反過來可以),因為Python並不清楚使用何種編碼格式將字符串轉換為字節序列:print('hello %s' % b'world') print(b'hello %s' % 'world') Traceback (most recent call last): File "/python/bytes_str.py", line 50, in <module> print(b'hello %s' % 'world') TypeError: %b requires a bytes-like object, or an object that implements __bytes__, not 'str'第2個問題是涉及文件句柄的操作(由打開的內置函數返回),寫文件時默認Unicode字符串而不是字節序列。這可能會導致拋出異常,尤其是對於習慣了Python 2的程式設計師而言。例如,假設我要向文件中寫入一些二進位數據,下面的代碼會拋出異常:with open('data.bin', 'w') as f: f.write(b'\xf1\xf2\xf3\xf4\xf5')Traceback (most recent call last): File "/python/bytes_str.py", line 53, in <module> f.write(b'\xf1\xf2\xf3\xf4\xf5')TypeError: write() argument must be str, not bytes拋出異常的原因是該文件是以寫文本模式('w')而不是寫二進位模式('wb')打開的。當文件處於文本模式時,寫操作期望字符串包含Unicode數據,而不是字節序列。所以為了避免拋出異常,應該用「wb」模式打開data.bin文件。with open('data.bin', 'wb') as f: f.write(b'\xf1\xf2\xf3\xf4\xf5')從文件讀取數據也存在類似的問題。例如,下面的代碼嘗試讀取data.bin文件的內容:with open('data.bin', 'r') as f: data = f.read()Traceback (most recent call last): File "/python/bytes_str.py", line 61, in <module> data = f.read() File "/Users/lining/opt/anaconda3/lib/python3.7/codecs.py", line 322, in decode (result, consumed) = self._buffer_decode(data, self.errors, final)UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 0: invalid continuation byte失敗是因為文件是在讀取文本模式('r')而非讀取二進位模式('rb')中打開的。當句柄處於文本模式時,它將使用系統的默認文本編碼來使用bytes.encode(用於寫入)和str.decode(用於讀取)方法來解釋二進位數據。在大多數系統上,默認編碼為UTF-8,該編碼不能接受二進位數據b'\ xf1 \ xf2 \ xf3 \ xf4 \ xf5',因此會拋出異常。所以應該使用「rb」模式來打開二進位文件。with open('data.bin', 'rb') as f: data = f.read() assert data == b'\xf1\xf2\xf3\xf4\xf5' 另外,還可以為open函數明確指定encoding參數(編碼格式),以確保Python可以正確處理二進位的編碼格式。例如,下面的代碼明確指定了使用cp1252編碼格式以只讀方式打開data.bin文件。with open('data.bin', 'r', encoding='cp1252') as f: data = f.read() print(data)(1)字節序列(bytes)包含8位的二進位數據,字符串(str)包含Unicode編碼的值;(2)為了讓程序更健壯,需要使用專門的函數來校驗輸入的是字節序列,還是字符串。如前面的to_bytes函數和to_str函數;(3)字節序列和字符串不能混合在一起進行運算(如+、>、<、%等);(4)如果你想讀寫二進位格式的文件,應該使用二進位模式打開文件(例如,"rb"或"wb");(5)如果你想讀寫文本格式的文件,需要考慮文本的編碼格式。需要顯式通過encoding參數傳入正確的編碼格式;對本文感興趣,可以加李寧老師微信公眾號(unitymarvel):關注 「極客起源」 公眾號,獲得更多免費技術視頻和文章。