學習筆記一:改善 Python 程序的 91 個建議

學習筆記一:《編寫高質量代碼 改善 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 個原則

  1. 函數設計要盡量短小,嵌套層次不宜過深

  2. 函數申明應該做到合理、簡單、易於使用

  3. 函數參數設計應該考慮向下兼容

  4. 一個函數只做一件事,盡量保證函數語句粒度的一致性

Python 中函數設計的好習慣還包括:不要在函數中定義可變對象作為默認值,使用異常替換返回錯誤,保證通過單元測試等。

# 關於函數設計的向下兼容def readfile(filename): # 第一版本 passdef readfile(filename, log): # 第二版本 passdef 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 datadef 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] = valueimport syssys.modules[__name__] = _const()import constconst.MY_CONSTANT = 1const.MY_SECOND_CONSTANT = 2const.MY_THIRD_CONSTANT = "a"const.MY_FORTH_CONSTANT = "b"

其他模塊中引用這些常量時,按照如下方式進行即可:

from constant import constprint(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,在內存中執行的順序如下:

  1. 先計算右邊的表達式y, x,因此先在內存中創建元組(y, x),其標識符和值分別為y, x及其對應的值,其中y和x是在初始化已經存在於內存中的對象

  2. 計算表達式左邊的值並進行賦值,元組被依次分配給左邊的標識符,通過解壓縮,元組第一標識符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.Spring0# 但通過以上方式實現枚舉都有不合理的地方>>> 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 addTypeError: 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()>>> nInteger 0>>> print(n)0>>> m = UserInt(2)>>> print(m)2>>> type(n) is intFalse # 顯然不合理>>> isinstance(n, int)True

我們可以使用isinstance來檢查:isinstance(object, classinfo)

建議 13:盡量轉換為浮點類型後再做除法

# 計算平均成績績點>>> gpa = ((4*96+3*85+5*98+2*70)*4) / ((4+3+5+2)*100)>>> gpa3.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 Videos0>>> 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: aindex: 1 element: bindex: 2 element: cindex: 3 element: dindex: 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 == s2True>>> s1 is s2False>>> s1.__eq__(s2)True>>> a = "Hi">>> b = "Hi">>> a == bTrue>>> a is bTrue

咦~怎麼上例中的a, b又是「同一對象」了?這跟 Python 的 string interning 機制有關,為了提高系統性能,對於較小的字元串會保留其值的一個副本,當創建新的字元串時直接指向該副本,所以a和b的 id 值是一樣的,同樣對於小整數[-5, 257)也是如此:

>>> id(a)140709793837832>>> id(b)140709793837832>>> x = -5>>> y = -5>>> x is yTrue>>> 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()查看預載入的模塊信息,當載入一個模塊時,解釋器實際上完成了如下動作:

  1. 在sys.modules中搜索該模塊是否存在,如果存在就導入到當前局部命名空間,如果不存在就為其創建一個字典對象,插入到sys.modules中

  2. 載入前確認是否需要對模塊對應的文件進行編譯,如果需要則先進行編譯

  3. 執行動態載入,在當前命名空間中執行編譯後的位元組碼,並將其中所有的對象放入模塊對應的字典中

>>> dir()["__builtins__", "__doc__", "__loader__", "__name__", "__package__", "__spec__"]>>> import testtesting 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>>> +++22>>> ---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>

  1. 計算表達式的值,返回一個上下文管理器對象

  2. 載入上下文管理器對象的__exit__()以備後用

  3. 調用上下文管理器對象的__enter__()

  4. 將__enter__()的返回值賦給目標對象

  5. 執行代碼塊,正常結束調用__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 number3 is a prime number5 is a prime number

可以看出,else 子句在循環正常結束和循環條件不成立時被執行,由 break 語句中斷時不執行,同樣,我們可以利用這顆語法糖作用在 while 和 try...except 中。

待更新。

推薦閱讀:

乾貨|Scikit-Learn的五種機器學習方法使用案例(python代碼)
文獻引文分析利器 HistCite 詳細使用教程(精簡易用免安裝版本 HistCite Pro 首發頁面)
OnlineJudge 2.0發布
爬蟲帶你逛知乎(下篇)
10min手寫(一):伺服器內存監控系統

TAG:Python | Python入门 | Python教程 |