大家都應該要知道的改善 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 = ((496+385+598+270)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學習資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章