摘選改善Python程式的91個建議
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)
8、利用assert語句來發現問題
>>> y = 2 >>> assertx == 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、資料交換不推薦使用中間變數x, y = 1, 2
x, y = y, x
原理:右值建立元組,左值接收元組的對應元素。
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 中加入了列舉,僅在父類沒有任何列舉成員的時候才允許繼承View Code
12、不推薦使用type來進行型別檢查、而使用isinstance()
14、警惕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")') # 嘿嘿嘿View Code
如果確實需要使用eval,建議使用安全性更好的ast.literal_eval。
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中 載入前確認是否需要對模組對應的檔案進行編譯,如果需要則先進行編譯 執行動態載入,在當前名稱空間中執行編譯後的位元組碼,並將其中所有的物件放入模組對應的字典中View Code
>>> 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__'])View Code
從上可以看出,對於使用者自定義的模組,import 機制會建立一個新的 module 將其加入當前的區域性名稱空間中,同時在 sys.modules 也加入該模組的資訊,但本質上是在引用同一個物件,通過test.py所在的目錄會多一個位元組碼檔案。
20、優先使用absolute import 來匯入模組
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 numberView Code
可以看出,else 子句在迴圈正常結束和迴圈條件不成立時被執行,由 break 語句中斷時不執行,同樣,我們可以利用這顆語法糖作用在 while 和 try...except 中。
31、記住函式傳參既不是傳值也不是引用
正確的說法是傳物件(call by object)或傳物件的引用(call-by-object-reference),函式引數在傳遞過程中將整個物件傳入,對可變物件的修改在函式外部以及內部都可見,對不可變物件的”修改“往往是通過生成一個新物件然是賦值實現的。
39、使用 Counter 進行計數統計 常見的計數統計可以使用dict、defaultdict、set和list,不過 Python 提供了一個更優雅的方式:>>> from collections import Counter >>> some_data = {'a', '2', 2, 3, 5, 'c', '7', 4, 5, 'd', 'b'} >>> Counter(some_data) Counter({'7',: 1, 2: 1, 3: 1, 4: 1, 5: 1, '2': 1, 'b': 1, 'a': 1, 'd': 1, 'c': 1})View Code
Counter 類屬於字典類的子類,是一個容器物件,用來統計雜湊物件,支援+、-、&、|,其中&和|分別返回兩個 Counter 物件各元素的最小值和最大值。
# 初始化 Counter('success') Counter(s=3, c=2, e=1, u=1) Counter({'s': 3, 'c': 2, 'u': 1, 'e': 1}) # 常用方法 list(Counter(some_data).elements()) # 獲取 key 值 Counter(some_data).most_common(2) # 前 N 個出現頻率最高的元素以及對應的次數 (Counter(some_data))['y'] # 訪問不存在的元素返回 0 c = Counter('success') c.update('successfully') # 更新統計值 c.subtract('successfully') # 統計數相減,允許為0或為負View Code
41、使用 argparse 處理命令列引數
import argparse parse = argparse.ArgumentParser() parse.add_argument('-o', '--output') parse.add_argument('-v', dest='verbose', action='store_true') args = parser.parse_args()View Code
42、使用pandas處理大型CSV檔案 教程
reader(csvfile[, dialect='excel'][, fmtparam]) # 讀取一個 csv 檔案,返回一個 reader 物件 csv.writer(csvfile, dialect='excel', **fmtparams) # 寫入 csv 檔案 csv.DictWriter(csvfile, fieldnames, restval='', extrasaction='raise', dialect='excel')常用API
43、一般情況下使用 ElementTree 解析 XML 教程
count = 0 for event, elem in ET.iterparse('test.xml'): if event == 'end': if elem.tag == 'userid': count += 1 elem.clear() print(count)View Code
45、使用 traceback 獲取棧資訊
當發生異常,開發人員往往需要看到現場資訊,trackback 模組可以滿足這個需求
traceback.print_exc() # 列印錯誤型別、值和具體的trace資訊 traceback.print_exception(type, value, traceback[, limit[, file]]) # 前三個引數的值可以從sys.exc_info() raceback.print_exc([limit[, file]]) # 同上,不需要傳入那麼多引數 traceback.format_exc([limit]) # 同 print_exc(),返回的是字串 traceback.extract_stack([file, [, limit]]) # 從當前棧中提取 trace 資訊View Code
traceback 模組獲取異常相關的資料是通過sys.exc_info()得到的,該函式返回異常型別type、異常value、呼叫和堆疊資訊traceback組成的元組。
同時 inspect 模組也提供了獲取 traceback 物件的介面。
50、利用模組實現單例模式
51、用 mixin 模式讓程式更加靈活
模板方法模式就是在一個方法中定義一個演算法的骨架,並將一些實現步驟延遲到子類中。模板方法可以使子類在不改變演算法結構的情況下,重新定義演算法中的某些步驟。看個例子:
class People(object): def make_tea(self): teapot = self.get_teapot() teapot.put_in_tea() teapot.put_in_water() return teapotView Code
顯然get_teapot()方法並不需要預先定義,也就是說我們的基類不需要預先申明抽象方法,子類只需要繼承 People 類並實現get_teapot(),這給除錯程式碼帶來了便利。但我們又想到如果一個子類 StreetPeople 描述的是正走在街上的人,那這個類將不會實現get_teapot(),一呼叫make_tea()就會產生找不到get_teapot()的 AttributeError,所以此時程式設計師應該立馬想到,隨著需求的增多,越來越多的 People 子類會選擇不喝茶而喝咖啡,或者是抽雪茄之類的,按照以上的思路,我們的程式碼只會變得越發難以維護。
所以我們希望能夠動態生成不同的例項:
class UseSimpleTeapot(object): def get_teapot(self): return SimpleTeapot() class UseKungfuTeapot(object): def get_teapot(self): return KungfuTeapot() class OfficePeople(People, UseSimpleTeapot): pass class HomePeople(People, UseSimpleTeapot): pass class Boss(People, UseKungfuTeapot): pass def simple_tea_people(): people = People() people.__base__ += (UseSimpleTeapot,) return people def coffee_people(): people = People() people.__base__ += (UseCoffeepot,) def tea_and_coffee_people(): people = People() people.__base__ += (UseSimpleTeapot, UserCoffeepot,) return people def boss(): people = People() people.__base__ += (KungfuTeapot, UseCoffeepot, ) return peopleView Code
以上程式碼的原理在於每個類都有一個__bases__屬性,它是一個元組,用來存放所有的基類,作為動態語言,Python 中的基類可以在執行中可以動態改變。所以當我們向其中增加新的基類時,這個類就擁有了新的方法,這就是混入mixin。
利用這個技術我們可以在不修改程式碼的情況下就可以完成需求:import mixins # 把員工需求定義在 Mixin 中放在 mixins 模組 def staff(): people = People() bases = [] for i in config.checked(): bases.append(getattr(maxins, i)) people.__base__ += tuple(bases) return peopleView Code
52、用釋出訂閱模式實現鬆耦合
釋出訂閱模式是一種程式設計模式,訊息的傳送者不會發送其訊息給特定的接收者,而是將釋出的訊息分為不同的類別直接釋出,並不關注訂閱者是誰。而訂閱者可以對一個或多個類別感興趣,且只接收感興趣的訊息,並且不關注是哪個釋出者釋出的訊息。要實現這個模式,就需要一箇中間代理人 Broker,它維護著釋出者和訂閱者的關係,訂閱者把感興趣的主題告訴它,而釋出者的資訊也通過它路由到各個訂閱者處。
from collections import defaultdict route_table = defaultdict(list) def sub(topic, callback): if callback in route_table[topic]: return route_table[topic].append(callback) def pub(topic, *args, **kw): for func in route_table[topic]: func(*args, **kw)Broker.py
將以上程式碼放在 Broker.py 的模組,省去了各種引數檢測、優先處理、取消訂閱的需求,只向我們展示釋出訂閱模式的基礎實現:
import Broker def greeting(name): print('Hello, {}'.format(name)) Broker.sub('greet', greeting) Broker.pub('greet', 'LaiYonghao')View Code
注意學習 blinker 和 python-message 兩個模組。
a