改善 Python 程式的 91 個建議(一)
第 1 章 引論
建議 1:理解 Pythonic 概念
Pythonic
Tim Peters 的 《The Zen of Python》相信學過 Python 的都耳熟能詳,在互動式環境中輸入import this可以檢視,其實有意思的是這段 Python 之禪的原始碼:
d = {} for c in (65, 97): for i in range(26): d[chr(i+c)] = chr((i+13) % 26 + c) print "".join([d.get(c, c) for c in s])
哈哈哈,相信這是大佬在跟我們舉反例吧。
書中還舉了一個快排的例子:
def quicksort(array): less = [] greater = [] if len(array) <= 1: return array pivot =array.pop() for x in array: if x <= pivot: less.append(x) else: greater.append(x) return quicksort(less) + [pivot] + quicksort(greater)
程式碼風格
通過對語法、庫和應用程式的理解來編寫程式碼,充分體現 Python 自身的特色:
# 變數交換 a, b = b, a # 上下文管理 with open(path, 'r') as f: do_sth_with(f) # 不應當過分地追求奇技淫巧 a = [1, 2, 3, 4] a[::-1] # 不推薦。好吧,自從學了切片我一直用的這個 list(reversed(a)) # 推薦
然後表揚了 Flask 框架,提到了 generator 之類的特性尤為 Pythonic,有個包和模組的約束:
- 包和模組的命名採用小寫、單數形式,而且短小
- 包通常僅作為名稱空間,如只含空的__init__.py檔案
建議 2:編寫 Pythonic 程式碼
命名的規範:
def find_num(searchList, num): for listValue in searchList: if num == listValue: return True else: pass
嘗試去通讀官方手冊,掌握不斷髮展的新特性,這將使你編寫程式碼的執行效率更高,推薦深入學習 Flask、gevent 和 requests。
建議 3:理解 Python 與 C 語言的不同之處
提到了三點:
- Python 使用程式碼縮排的方式來分割程式碼塊,不要混用 Tab 鍵和空格
- Python 中單、雙引號的使用
- 三元操作符:x if bool else y
建議 4:在程式碼中適當新增註釋
這一點已經受教了,現在編寫程式碼都會合理地加入塊註釋、行註釋和文件註釋,可以使用__doc__輸出。
建議 5:通過適當新增空行使程式碼佈局更為優雅、合理
建議 6:編寫函式的 4 個原則
- 函式設計要儘量短小,巢狀層次不宜過深
- 函式申明應該做到合理、簡單、易於使用
- 函式引數設計應該考慮向下相容
- 一個函式只做一件事,儘量保證函式語句粒度的一致性
Python 中函式設計的好習慣還包括:不要在函式中定義可變物件作為預設值,使用異常替換返回錯誤,保證通過單元測試等。
# 關於函式設計的向下相容 def readfile(filename): # 第一版本 pass def readfile(filename, log): # 第二版本 pass def readfile(filename, logger=logger.info): # 合理的設計 pass
最後還有個函式可讀性良好的例子:
def GetContent(ServerAdr, PagePath): http = httplib.HTTP(ServerAdr) http.putrequest('GET', PagePath) http.putheader('Accept', 'text/html') http.putheader('Accept', 'text/plain') http.endheaders() httpcode, httpmsg, headers = http.getreply() if httpcode != 200: raise "Could not get document: Check URL and Path." doc = http.getfile() data = doc.read() # 此處是不是應該使用 with ? doc.close return data def ExtractData(inputstring, start_line, end_line): lstr = inputstring.splitlines() # split j = 0 for i in lstr: j += 1 if i.strip() == start_line: slice_start = j elif i.strip() == end_line: slice_end = j return lstr[slice_start:slice_end] def SendEmail(sender, receiver, smtpserver, username, password, content): subject = "Contented get from the web" msg = MIMEText(content, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') smtp = smtplib.SMTP() smtp.connect(smtpserver) smtp.login(username, password) smtp.sendmail(sender, receiver, msg.as_string()) smtp.quit()
建議 7:將常量集中到一個檔案
在 Python 中應當如何使用常量:
- 通過命名風格提醒使用者該變數代表常量,如常量名全部大寫
- 通過自定義類實現常量功能:將存放常量的檔案命名為constant.py,並在其中定義一系列常量
class _const: class ConstError(TypeError): pass class ConstCaseError(ConstError): pass def __setattr__(self, name, value): if self.__dict__.has_key(name): raise self.ConstError, "Can't change const.%s" % name if not name.isupper(): raise self.ConstCaseError, \ 'const name "%s" is not all uppercase' % name self.__dict__(name) = value import sys sys.modules[__name__] = _const() import const const.MY_CONSTANT = 1 const.MY_SECOND_CONSTANT = 2 const.MY_THIRD_CONSTANT = 'a' const.MY_FORTH_CONSTANT = 'b'
其他模組中引用這些常量時,按照如下方式進行即可:
from constant import const print(const.MY_CONSTANT)
第 2 章 程式設計慣用法
建議 8:利用 assert 語句來發現問題
>>> y = 2 >>> assert x == y, "not equals" Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: not equals >>> x = 1 >>> y = 2 # 以上程式碼相當於 >>> if __debug__ and not x == y: ... raise AssertionError("not equals") ... Traceback (most recent call last): File "<stdin>", line 2, in <module> AssertionError: not equals
執行是加入-O引數可以禁用斷言。
建議 9:資料交換的時候不推薦使用中間變數
>>> Timer('temp = x; x = y; y = temp;', 'x = 2; y = 3').timeit() 0.059251302998745814 >>> Timer('x, y = y, x', 'x = 2; y = 3').timeit() 0.05007316499904846
對於表示式x, y = y, x,在記憶體中執行的順序如下:
- 先計算右邊的表示式y, x,因此先在記憶體中建立元組(y, x),其識別符號和值分別為y, x及其對應的值,其中y和x是在初始化已經存在於記憶體中的物件
- 計算表示式左邊的值並進行賦值,元組被依次分配給左邊的識別符號,通過解壓縮,元組第一識別符號y分配給左邊第一個元素x,元組第二識別符號x分配給左邊第一個元素y,從而達到交換的目的
下面是通過位元組碼的分析:
>>> import dis >>> def swap1(): ... x = 2 ... y = 3 ... x, y = y, x ... >>> def swap2(): ... x = 2 ... y = 3 ... temp = x ... x = y ... y = temp ... >>> dis.dis(swap1) 2 0 LOAD_CONST 1 (2) 3 STORE_FAST 0 (x) 3 6 LOAD_CONST 2 (3) 9 STORE_FAST 1 (y) 4 12 LOAD_FAST 1 (y) 15 LOAD_FAST 0 (x) 18 ROT_TWO # 交換兩個棧的最頂層元素 19 STORE_FAST 0 (x) 22 STORE_FAST 1 (y) 25 LOAD_CONST 0 (None) 28 RETURN_VALUE >>> dis.dis(swap2) 2 0 LOAD_CONST 1 (2) 3 STORE_FAST 0 (x) 3 6 LOAD_CONST 2 (3) 9 STORE_FAST 1 (y) 4 12 LOAD_FAST 0 (x) 15 STORE_FAST 2 (temp) 5 18 LOAD_FAST 1 (y) 21 STORE_FAST 0 (x) 6 24 LOAD_FAST 2 (temp) 27 STORE_FAST 1 (y) 30 LOAD_CONST 0 (None) 33 RETURN_VALUE
建議 10:充分利用 Lazy evaluation 的特性
def fib(): a, b = 0, 1 while True: yield a a, b = b, a + b
哈哈哈,我猜到肯定是生成器實現菲波拉契序列的例子,不過對比我寫的版本,唉。。。
建議 11:理解列舉替代實現的缺陷
利用 Python 的動態特徵,可以實現列舉:
# 方式一 class Seasons: Spring, Summer, Autumn, Winter = range(4) # 方式二 def enum(*posarg, **keysarg): return type("Enum", (object,), dict(zip(posarg, range(len(posarg))), **keysarg)) Seasons = enum("Spring", "Summer", "Autumn", Winter=1) Seasons.Spring # 方式三 >>> from collections import namedtuple >>> Seasons = namedtuple('Seasons', 'Spring Summer Autumn Winter')._make(range(4)) >>> Seasons.Spring 0 # 但通過以上方式實現列舉都有不合理的地方 >>> Seasons._replace(Spring=2) │ Seasons(Spring=2, Summer=1, Autumn=2, Winter=3) # Python3.4 中加入了列舉,僅在父類沒有任何列舉成員的時候才允許繼承
建議 12:不推薦使用 type 來進行型別檢查
作為動態語言,Python 直譯器會在執行時自動進行型別檢查並根據需要進行隱式型別轉換,當變數型別不同而兩者之間又不能進行隱式型別轉換時便丟擲TypeError異常。
>>> def add(a, b): ... return a + b ... >>> add(1, 2j) (1+2j) >>> add('a', 'b') 'ab' >>> add(1, 2) 3 >>> add(1.0, 2.3) 3.3 >>> add([1, 2], [3, 4]) [1, 2, 3, 4] >>> add(1, 'a') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in add TypeError: unsupported operand type(s) for +: 'int' and 'str'
所以實際應用中,我們常常需要進行型別檢查,但是不推薦使用type(),因為基於內建型別擴充套件的使用者自定義型別,type()並不能準確返回結果:
class UserInt(int): def __init__(self, val=0): self._val = int(val) def __add__(self, val): if isinstance(val, UserInt): return UserInt(self._val + val._val) return self._val + val def __iadd__(self, val): raise NotImplementedError("not support operation") def __str__(self): return str(self._val) def __repr__(self): return "Integer %s" % self._val >>> n = UserInt() >>> n Integer 0 >>> print(n) 0 >>> m = UserInt(2) >>> print(m) 2 >>> type(n) is int False # 顯然不合理 >>> isinstance(n, int) True
我們可以使用isinstance來檢查:isinstance(object, classinfo)
建議 13:儘量轉換為浮點型別後再做除法
# 計算平均成績績點 >>> gpa = ((4*96+3*85+5*98+2*70)*4) / ((4+3+5+2)*100) >>> gpa 3.625714285714286 # 終於知道自己的績點是咋算的了
建議 14:警惕 eval() 的安全漏洞
eval(expression[, globals[, locals]])將字串 str 當成有效的表示式來求值並返回計算結果,globas為字典形式,locals為任何對映物件,它們分別表示全域性和區域性名稱空間,兩者都省略表示式將在呼叫的環境中執行,為什麼需要警惕eval()呢:
# 合理正確地使用 >>> eval("1+1==2") True >>> eval('"a"+"b"') 'ab' # 壞心眼的geek >>> eval('__import__("os").system("dir")') Desktop Documents Downloads examples.desktop Music Pictures Public __pycache__ Templates Videos 0 >>> eval('__import__("os").system("del * /Q")') # 嘿嘿嘿
如果確實需要使用eval,建議使用安全性更好的ast.literal_eval。
建議 15:使用 enumerate() 獲取序列迭代的索引和值
>>> li = ['a', 'b', 'c', 'd', 'e'] >>> for i, e in enumerate(li): ... print('index: ', i, 'element: ', e) ... index: 0 element: a index: 1 element: b index: 2 element: c index: 3 element: d index: 4 element: e # enumerate(squence, start=0) 內部實現 def enumerate(squence, start=0): n = start for elem in sequence: yield n, elem # 666 n += 1 # 明白了原理我們自己也來實現一個反序的 def reversed_enumerate(squence): n = -1 for elem in reversed(sequence): yield len(sequence) + n, elem n -= 1
建議 16:分清 == 與 is 的適用場景
操作符意義isobject identity==equal
is的作用是用來檢查物件的標示符是否一致,也就是比較兩個物件在記憶體中是否擁有同一塊記憶體空間,相當於id(x) == id(y),它並不適用於判斷兩個字串是否相等。==才是用來判斷兩個物件的值是否相等,實際是呼叫了內部的__eq__,所以a==b相當於a.__eq__(b),也就是說==是可以被過載的,而is不能被過載。
>>> s1 = 'hello world' >>> s2 = 'hello world' >>> s1 == s2 True >>> s1 is s2 False >>> s1.__eq__(s2) True >>> a = 'Hi' >>> b = 'Hi' >>> a == b True >>> a is b True
咦~怎麼上例中的a, b又是“同一物件”了?這跟 Python 的 string interning 機制有關,為了提高系統效能,對於較小的字串會保留其值的一個副本,當建立新的字串時直接指向該副本,所以a和b的 id 值是一樣的,同樣對於小整數[-5, 257)也是如此:
>>> id(a) 140709793837832 >>> id(b) 140709793837832 >>> x = -5 >>> y = -5 >>> x is y True >>> id(x) == id(y) True
建議 17:考慮相容性,儘可能使用 Unicode
我之前也總結過編碼的問題。由於最早的編碼是 ASCII 碼,只能表示 128 個字元,顯然這對其它語言編碼並不適用,Unicode就是為了不同的文字分配一套統一的編碼。
建議 18:構建合理的包層次來管理 module
本質上每一個 Python 檔案都是一個模組,使用模組可以增強程式碼的可維護性和可重用性,在較大的專案中,我們需要合理地組織專案層次來管理模組,這就是包(Package)的作用。
一句話說包:一個包含__init__.py 檔案的目錄。包中的模組可以通過.進行訪問,即包名.模組名。那麼這個__init__.py檔案有什麼用呢?最明顯的作用就是它區分了包和普通目錄,在該檔案中申明模組級別的 import 語句從而變成了包級別可見,另外在該檔案中定義__all__變數,可以控制需要匯入的子包或模組。
這裡給出一個較為合理的包組織方式,是FlaskWeb 開發:基於Python的Web應用開發實戰一書中推薦而來的:
|-flasky |-app/ # Flask 程式 |-templates/ # 存放模板 |-static/ # 靜態檔案資源 |-main/ |-__init__.py |-errors.py # 藍本中的錯誤處理程式 |-forms.py # 表單物件 |-views.py # 藍本中定義的程式路由 |-__init__.py |-email.py # 電子郵件支援 |-models.py # 資料庫模型 |-migrations/ # 資料庫遷移指令碼 |-tests/ # 單元測試 |-__init__.py |-test*.py |-venv/ # 虛擬環境 |-requirements/ |-dev.txt # 開發過程中的依賴包 |-prod.txt # 生產過程中的依賴包 |-config.py # 儲存程式配置 |-manage.py # 啟動程式以及其他的程式任務
第 3 章:基礎語法
建議 19:有節制地使用 from...import 語句
Python 提供三種方式來引入外部模組:import語句、from...import語句以及__import__函式,其中__import__函式顯式地將模組的名稱作為字串傳遞並賦值給名稱空間的變數。
使用import需要注意以下幾點:
- 優先使用import a的形式
- 有節制地使用from a import A
- 儘量避免使用from a import *
為什麼呢?我們來看看 Python 的 import 機制,Python 在初始化執行環境的時候會預先載入一批內建模組到記憶體中,同時將相關資訊存放在sys.modules中,我們可以通過sys.modules.items()檢視預載入的模組資訊,當載入一個模組時,直譯器實際上完成了如下動作:
- 在sys.modules中搜尋該模組是否存在,如果存在就匯入到當前區域性名稱空間,如果不存在就為其建立一個字典物件,插入到sys.modules中
- 載入前確認是否需要對模組對應的檔案進行編譯,如果需要則先進行編譯
- 執行動態載入,在當前名稱空間中執行編譯後的位元組碼,並將其中所有的物件放入模組對應的字典中
>>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__'] >>> import test testing module import >>> dir() ['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'test'] >>> import sys >>> ‘test’ in sys.modules.keys() True >>> id(test) 140367239464744 >>> id(sys.modules['test']) 140367239464744 >>> dir(test) ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b'] >>> sys.modules['test'].__dict__.keys() dict_keys(['__file__', '__builtins__', '__doc__', '__loader__', '__package__', '__spec__', '__name__', 'b', 'a', '__cached__'])
從上可以看出,對於使用者自定義的模組,import 機制會建立一個新的 module 將其加入當前的區域性名稱空間中,同時在 sys.modules 也加入該模組的資訊,但本質上是在引用同一個物件,通過test.py所在的目錄會多一個位元組碼檔案。
建議 20:優先使用 absolute import 來匯入模組
建議 21: i+=1 不等於 ++i
首先++i或--i在 Python 語法上是合法,但並不是我們通常理解的自增或自減操作:
>>> ++1 # +(+1) 1 >>> --1 # -(-1) 1 >>> +++2 2 >>> ---2 -2
原來+或-只表示正負數符號。
建議 22:使用 with 自動關閉資源
對於開啟的資源我們記得關閉它,如檔案、資料庫連線等,Python 提供了一種簡單優雅的解決方案:with。
先來看with實現的原理吧。
with的實現得益於一個稱為上下文管理器(context manager)的東西,它定義程式執行時需要建立的上下文,處理程式的進入和退出,實現了上下文管理協議,即物件中定義了__enter__()和__exit__(),任何實現了上下文協議的物件都可以稱為一個上下文管理器:
- __enter__():返回執行時上下文相關的物件
- __exit__(exception_type, exception_value, traceback):退出執行時的上下文,處理異常、清理現場等
包含with語句的程式碼塊執行過程如下:
with 表示式 [as 目標]: 程式碼塊 # 例 >>> with open('test.txt', 'w') as f: ... f.write('test') ... 4 >>> f.__enter__ <built-in method __enter__ of _io.TextIOWrapper object at 0x7f1b967aaa68> >>> f.__exit__ <built-in method __exit__ of _io.TextIOWrapper object at 0x7f1b967aaa68>
- 計算表示式的值,返回一個上下文管理器物件
- 載入上下文管理器物件的__exit__()以備後用
- 呼叫上下文管理器物件的__enter__()
- 將__enter__()的返回值賦給目標物件
- 執行程式碼塊,正常結束呼叫__exit__(),其返回值直接忽略,如果發生異常,會呼叫__exit__()並將異常型別、值及 traceback 作為引數傳遞給__exit__(),__exit__()返回值為 false 異常將會重新丟擲,返回值為 true 異常將被掛起,程式繼續執行
於此,我們可以自定義一個上下文管理器:
>>> class MyContextManager(object): ... def __enter__(self): ... print('entering...') ... def __exit__(self, exception_type, exception_value, traceback): ... print('leaving...') ... if exception_type is None: ... print('no exceptions!') ... return False ... elif exception_type is ValueError: ... print('value error!') ... return True ... else: ... print('other error') ... return True ... >>> with MyContextManager(): ... print('Testing...') ... entering... Testing... leaving... no exceptions! >>> with MyContextManager(): ... print('Testing...') ... raise(ValueError) ... entering... Testing... leaving... value error!
Python 還提供contextlib模組,通過 Generator 實現,其中的 contextmanager 作為裝飾器來提供一種針對函式級別上的上下文管理器,可以直接作用於函式/物件而不必關心__enter__()和__exit__()的實現。
推薦文章
建議 23:使用 else 子句簡化迴圈(異常處理)
Python 的 else 子句提供了隱含的對迴圈是否由 break 語句引發迴圈結束的判斷,有點繞哈,來看例子:
>>> def print_prime(n): ... for i in range(2, n): ... for j in range(2, i): ... if i % j == 0: ... break ... else: ... print('{} is a prime number'.format(i)) ... >>> print_prime(7) 2 is a prime number 3 is a prime number 5 is a prime number
可以看出,else 子句在迴圈正常結束和迴圈條件不成立時被執行,由 break 語句中斷時不執行,同樣,我們可以利用這顆語法糖作用在 while 和 try...except 中。
相關文章
- 改善 Python 程式的 91 個建議Python
- 改善 Python 程式的 91 個建議(二)Python
- 改善 Python 程式的 91 個建議(三)Python
- 改善 Python 程式的 91 個建議(四)Python
- 改善 Python 程式的 91 個建議(六)Python
- 《改善python程式的91個建議》讀書筆記Python筆記
- 編寫高質量程式碼 改善Python程式的91個建議Python
- 讀改善c#程式碼157個建議:建議1~3C#
- 讀改善c#程式碼157個建議:建議4~6C#
- 讀改善c#程式碼157個建議:建議7~9C#
- 讀改善c#程式碼157個建議:建議10~12C#
- 讀改善c#程式碼157個建議:建議13~15C#
- Flutter 6 個建議改善你的程式碼結構Flutter
- 編寫高質量程式碼:改善Java程式的151個建議(第4章:字串___建議52~55)Java字串
- 編寫高質量程式碼:改善Java程式的151個建議(第4章:字串___建議56~59)Java字串
- 《編寫高質量程式碼:改善Java程式的151個建議》筆記Java筆記
- 程式設計師必備基礎:改善Java程式的20個實用建議程式設計師Java
- 編寫高質量程式碼:改善Java程式的151個建議(第2章:基本型別___建議21~25)Java型別
- 編寫高質量程式碼:改善Java程式的151個建議(第2章:基本型別___建議26~30)Java型別
- 改善網頁設計的10個絕佳SEO建議網頁
- 編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議93~97)Java泛型反射
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議60~64)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議65~69)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議70~74)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議75~78)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議79~82)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議41~46)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議47~51)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議31~35)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議36~40)Java物件
- 改善 ASP.NET MVC 程式碼庫的 5 點建議ASP.NETMVC
- 一個老程式設計師的建議程式設計師
- 改善Java文件的理由、建議和技巧Java
- 編寫高質量程式碼:改善Java程式的151個建議(第6章:列舉和註解___建議83~87)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第6章:列舉和註解___建議88~92)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議110~113)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議114~117)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議98~101)Java泛型反射