讓你的 Python 代碼優雅又地道
(點擊
上方藍字
,快速關注我們)
編譯:0xFEE1C001
www.lightxue.com/transforming-code-into-beautiful-idiomatic-python
如有好文章投稿,請點擊 → 這裡了解詳情
譯序
如果說優雅也有缺點的話,那就是你需要艱巨的工作才能得到它,需要良好的教育才能欣賞它。
—— Edsger Wybe Dijkstra
在Python社區文化的澆灌下,演化出了一種獨特的代碼風格,去指導如何正確地使用Python,這就是常說的pythonic。一般說地道(idiomatic)的python代碼,就是指這份代碼很pythonic。Python的語法和標準庫設計,處處契合著pythonic的思想。而且Python社區十分注重編碼風格一的一致性,他們極力推行和處處實踐著pythonic。所以經常能看到基於某份代碼P vs NP (pythonic vs non-pythonic)的討論。pythonic的代碼簡練,明確,優雅,絕大部分時候執行效率高。閱讀pythonic的代碼能體會到「代碼是寫給人看的,只是順便讓機器能運行」暢快。
然而什麼是pythonic,就像什麼是地道的漢語一樣,切實存在但標準模糊。import this可以看到Tim Peters提出的Python之禪,它提供了指導思想。許多初學者都看過它,深深贊同它的理念,但是實踐起來又無從下手。PEP 8給出的不過是編碼規範,對於實踐pythonic還遠遠不夠。如果你正被如何寫出pythonic的代碼而困擾,或許這份筆記能給你幫助。
Raymond Hettinger是Python核心開發者,本文提到的許多特性都是他開發的。同時他也是Python社區熱忱的佈道師,不遺餘力地傳授pythonic之道。這篇文章是網友Jeff Paine整理的他在2013年美國的PyCon的演講的筆記。
術語澄清:本文所說的集合全都指collection,而不是set。
以下是正文。
本文是Raymond Hettinger在2013年美國PyCon演講的筆記(視頻, 幻燈片)。
示例代碼和引用的語錄都來自Raymond的演講。這是我按我的理解整理出來的,希望你們理解起來跟我一樣順暢!
遍歷一個範圍內的數字
for
i
in
[
0
,
1
,
2
,
3
,
4
,
5
]
:
i *
*
2
for
i
in
range
(
6
)
:
i *
*
2
更好的方法
for
i
in
xrange
(
6
)
:
i *
*
2
xrange會返回一個迭代器,用來一次一個值地遍歷一個範圍。這種方式會比range更省內存。xrange在Python 3中已經改名為range。
遍歷一個集合
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
for
i
in
range
(
len
(
colors
))
:
colors
[
i
]
更好的方法
for
color
in
colors
:
color
反向遍歷
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
for
i
in
range
(
len
(
colors
)
-
1
,
-
1
,
-
1
)
:
colors
[
i
]
更好的方法
for
color
in
reversed
(
colors
)
:
color
遍歷一個集合及其下標
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
for
i
in
range
(
len
(
colors
))
:
i
,
"--->"
,
colors
[
i
]
更好的方法
for
i
,
color
in
enumerate
(
colors
)
:
i
,
"--->"
,
color
這種寫法效率高,優雅,而且幫你省去親自創建和自增下標。
當你發現你在操作集合的下標時,你很有可能在做錯事。
遍歷兩個集合
names
=
[
"raymond"
,
"rachel"
,
"matthew"
]
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
n
=
min
(
len
(
names
),
len
(
colors
))
for
i
in
range
(
n
)
:
names
[
i
],
"--->"
,
colors
[
i
]
for
name
,
color
in
zip
(
names
,
colors
)
:
name
,
"--->"
,
color
更好的方法
for
name
,
color
in
izip
(
names
,
colors
)
:
name
,
"--->"
,
color
zip在內存中生成一個新的列表,需要更多的內存。izip比zip效率更高。
注意:在Python 3中,izip改名為zip,並替換了原來的zip成為內置函數。
有序地遍歷
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
# 正序
for
color
in
sorted
(
colors
)
:
colors
# 倒序
for
color
in
sorted
(
colors
,
reverse
=
True
)
:
colors
自定義排序順序
colors
=
[
"red"
,
"green"
,
"blue"
,
"yellow"
]
def compare_length
(
c1
,
c2
)
:
if
len
(
c1
)
<
len
(
c2
)
:
return
-
1
if
len
(
c1
)
>
len
(
c2
)
:
return
1
return
0
print sorted
(
colors
,
cmp
=
compare_length
)
更好的方法
print sorted(colors, key=len)
第一種方法效率低而且寫起來很不爽。另外,Python 3已經不支持比較函數了。
調用一個函數直到遇到標記值
blocks
=
[]
while
True
:
block
=
f
.
read
(
32
)
if
block
==
""
:
break
blocks
.
append
(
block
)
更好的方法
blocks
=
[]
for
block
in
iter
(
partial
(
f
.
read
,
32
),
""
)
:
blocks
.
append
(
block
)
iter接受兩個參數。第一個是你反覆調用的函數,第二個是標記值。
譯註:這個例子里不太能看出來方法二的優勢,甚至覺得partial讓代碼可讀性更差了。方法二的優勢在於iter的返回值是個迭代器,迭代器能用在各種地方,set,sorted,min,max,heapq,sum……
在循環內識別多個退出點
def find
(
seq
,
target
)
:
found
=
False
for
i
,
value
in
enumerate
(
seq
)
:
if
value
==
target
:
found
=
True
break
if
not
found
:
return
-
1
return
i
更好的方法
def find
(
seq
,
target
)
:
for
i
,
value
in
enumerate
(
seq
)
:
if
value
==
target
:
break
else
:
return
-
1
return
i
for執行完所有的循環後就會執行else。
譯註:剛了解for-else語法時會困惑,什麼情況下會執行到else里。有兩種方法去理解else。傳統的方法是把for看作if,當for後面的條件為False時執行else。其實條件為False時,就是for循環沒被break出去,把所有循環都跑完的時候。所以另一種方法就是把else記成nobreak,當for沒有被break,那麼循環結束時會進入到else。
遍歷字典的key
d
=
{
"matthew"
:
"blue"
,
"rachel"
:
"green"
,
"raymond"
:
"red"
}
for
k
in
d
:
k
for
k
in
d
.
keys
()
:
if
k
.
startswith
(
"r"
)
:
del
d
[
k
]
什麼時候應該使用第二種而不是第一種方法?當你需要修改字典的時候。
如果你在迭代一個東西的時候修改它,那就是在冒天下之大不韙,接下來發生什麼都活該。
d.keys()把字典里所有的key都複製到一個列表裡。然後你就可以修改字典了。
注意:如果在Python 3里迭代一個字典你得顯示地寫:list(d.keys()),因為d.keys()返回的是一個「字典視圖」(一個提供字典key的動態視圖的迭代器)。詳情請看文檔。
遍歷一個字典的key和value
# 並不快,每次必須要重新哈希並做一次查找
for
k
in
d
:
k
,
"--->"
,
d
[
k
]
# 產生一個很大的列表
for
k
,
v
in
d
.
items
()
:
k
,
"--->"
,
v
更好的方法
for
k
,
v
in
d
.
iteritems
()
:
k
,
"--->"
,
v
iteritems()更好是因為它返回了一個迭代器。
注意:Python 3已經沒有iteritems()了,items()的行為和iteritems()很接近。詳情請看文檔。
用key-value對構建字典
names
=
[
"raymond"
,
"rachel"
,
"matthew"
]
colors
=
[
"red"
,
"green"
,
"blue"
]
d
=
dict
(
izip
(
names
,
colors
))
# {"matthew": "blue", "rachel": "green", "raymond": "red"}
Python 3: d = dict(zip(names, colors))
用字典計數
colors
=
[
"red"
,
"green"
,
"red"
,
"blue"
,
"green"
,
"red"
]
# 簡單,基本的計數方法。適合初學者起步時學習。
d
=
{}
for
color
in
colors
:
if
color
not
in
d
:
d
[
color
]
=
0
d
[
color
]
+=
1
# {"blue": 1, "green": 2, "red": 3}
更好的方法
d
=
{}
for
color
in
colors
:
d
[
color
]
=
d
.
get
(
color
,
0
)
+
1
# 稍微潮點的方法,但有些坑需要注意,適合熟練的老手。
d
=
defaultdict
(
int
)
for
color
in
colors
:
d
[
color
]
+=
1
用字典分組 — 第I部分和第II部分
names
=
[
"raymond"
,
"rachel"
,
"matthew"
,
"roger"
,
"betty"
,
"melissa"
,
"judith"
,
"charlie"
]
# 在這個例子,我們按name的長度分組
d
=
{}
for
name
in
names
:
key
=
len
(
name
)
if
key
not
in
d
:
d
[
key
]
=
[]
d
[
key
].
append
(
name
)
# {5: ["roger", "betty"], 6: ["rachel", "judith"], 7: ["raymond", "matthew", "melissa", "charlie"]}
d
=
{}
for
name
in
names
:
key
=
len
(
name
)
d
.
setdefault
(
key
,
[]).
append
(
name
)
更好的方法
d
=
defaultdict
(
list
)
for
name
in
names
:
key
=
len
(
name
)
d
[
key
].
append
(
name
)
字典的popitem()是原子的嗎?
d
=
{
"matthew"
:
"blue"
,
"rachel"
:
"green"
,
"raymond"
:
"red"
}
while
d
:
key
,
value
=
d
.
popitem
()
key
,
"-->"
,
value
popitem是原子的,所以多線程的時候沒必要用鎖包著它。
連接字典
defaults
=
{
"color"
:
"red"
,
"user"
:
"guest"
}
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
"-u"
,
"--user"
)
parser
.
add_argument
(
"-c"
,
"--color"
)
namespace
=
parser
.
parse_args
([])
command_line_args
=
{
k
:
v
for
k
,
v
in
vars
(
namespace
).
items
()
if
v
}
# 下面是通常的作法,默認使用第一個字典,接著用環境變數覆蓋它,最後用命令行參數覆蓋它。
# 然而不幸的是,這種方法拷貝數據太瘋狂。
d
=
defaults
.
copy
()
d
.
update
(
os
.
environ
)
d
.
update
(
command_line_args
)
更好的方法
d = ChainMap(command_line_args, os.environ, defaults)
ChainMap在Python 3中加入。高效而優雅。
提高可讀性
位置參數和下標很漂亮
但關鍵字和名稱更好
第一種方法對計算機來說很便利
第二種方法和人類思考方式一致
用關鍵字參數提高函數調用的可讀性
twitter_search("@obama", False, 20, True)
更好的方法
twitter_search("@obama", retweets=False, numtweets=20, popular=True)
第二種方法稍微(微秒級)慢一點,但為了代碼的可讀性和開發時間,值得。
用namedtuple提高多個返回值的可讀性
# 老的testmod返回值
doctest
.
testmod
()
# (0, 4)
# 測試結果是好是壞?你看不出來,因為返回值不清晰。
更好的方法
# 新的testmod返回值, 一個namedtuple
doctest
.
testmod
()
# TestResults(failed=0, attempted=4)
namedtuple是tuple的子類,所以仍適用正常的元組操作,但它更友好。
創建一個nametuple
TestResults = namedTuple("TestResults", ["failed", "attempted"])
unpack序列
p
=
"Raymond"
,
"Hettinger"
,
0x30
,
"python@example.com"
# 其它語言的常用方法/習慣
fname
=
p
[
0
]
lname
=
p
[
1
]
age
=
p
[
2
]
=
p
[
3
]
更好的方法
fname, lname, age, email = p
第二種方法用了unpack元組,更快,可讀性更好。
更新多個變數的狀態
def fibonacci
(
n
)
:
x
=
0
y
=
1
for
i
in
range
(
n
)
:
x
t
=
y
y
=
x
+
y
x
=
t
更好的方法
def fibonacci
(
n
)
:
x
,
y
=
0
,
1
for
i
in
range
(
n
)
:
x
x
,
y
=
y
,
x
+
y
第一種方法的問題
x和y是狀態,狀態應該在一次操作中更新,分幾行的話狀態會互相對不上,這經常是bug的源頭。
操作有順序要求
太底層太細節
第二種方法抽象層級更高,沒有操作順序出錯的風險而且更效率更高。
同時狀態更新
tmp_x
=
x
+
dx *
t
tmp_y
=
y
+
dy *
t
tmp_dx
=
influence
(
m
,
x
,
y
,
dx
,
dy
,
partial
=
"x"
)
tmp_dy
=
influence
(
m
,
x
,
y
,
dx
,
dy
,
partial
=
"y"
)
x
=
tmp
_
xy
=
tmp_y
dx
=
tmp_dx
dy
=
tmp_dy
更好的方法
x
,
y
,
dx
,
dy
=
(
x
+
dx *
t
,
y
+
dy *
t
,
influence
(
m
,
x
,
y
,
dx
,
dy
,
partial
=
"x"
),
influence
(
m
,
x
,
y
,
dx
,
dy
,
partial
=
"y"
))
效率
優化的基本原則
除非必要,別無故移動數據
稍微注意一下用線性的操作取代O(n**2)的操作
總的來說,不要無故移動數據
連接字元串
names
=
[
"raymond"
,
"rachel"
,
"matthew"
,
"roger"
,
"betty"
,
"melissa"
,
"judith"
,
"charlie"
]
s
=
names
[
0
]
for
name
in
names
[
1
:
]
:
s
+=
", "
+
name
s
更好的方法
print ", ".join(names)
更新序列
names
=
[
"raymond"
,
"rachel"
,
"matthew"
,
"roger"
,
"betty"
,
"melissa"
,
"judith"
,
"charlie"
]
del
names
[
0
]
# 下面的代碼標誌著你用錯了數據結構
names
.
pop
(
0
)
names
.
insert
(
0
,
"mark"
)
更好的方法
names
=
deque
([
"raymond"
,
"rachel"
,
"matthew"
,
"roger"
,
"betty"
,
"melissa"
,
"judith"
,
"charlie"
])
# 用deque更有效率
del
names
[
0
]
names
.
popleft
()
names
.
appendleft
(
"mark"
)
裝飾器和上下文管理
用於把業務和管理的邏輯分開
分解代碼和提高代碼重用性的乾淨優雅的好工具
起個好名字很關鍵
記住蜘蛛俠的格言:能力越大,責任越大
使用裝飾器分離出管理邏輯
# 混著業務和管理邏輯,無法重用
def web_lookup
(
url
,
saved
=
{})
:
if
url
in
saved
:
return
saved
[
url
]
page
=
urllib
.
urlopen
(
url
).
read
()
saved
[
url
]
=
page
return
page
更好的方法
@cache
def web_lookup
(
url
)
:
return
urllib
.
urlopen
(
url
).
read
()
注意:Python 3.2開始加入了functools.lru_cache解決這個問題。
分離臨時上下文
# 保存舊的,創建新的
old_context
=
getcontext
().
copy
()
getcontext
().
prec
=
50
print Decimal
(
355
)
/
Decimal
(
113
)
setcontext
(
old_context
)
更好的方法
with localcontext
(
Context
(
prec
=
50
))
:
print Decimal
(
355
)
/
Decimal
(
113
)
譯註:示例代碼在使用標準庫decimal,這個庫已經實現好了localcontext。
如何打開關閉文件
f
=
open
(
"data.txt"
)
try
:
data
=
f
.
read
()
finally
:
f
.
close
()
更好的方法
with open
(
"data.txt"
)
as
f
:
data
=
f
.
read
()
如何使用鎖
# 創建鎖
lock
=
threading
.
Lock
()
# 使用鎖的老方法
lock
.
acquire
()
try
:
"Critical section 1"
"Critical section 2"
finally
:
lock
.
release
()
更好的方法
# 使用鎖的新方法
with
lock
:
"Critical section 1"
"Critical section 2"
分離出臨時的上下文
try
:
os
.
remove
(
"somefile.tmp"
)
except
OSError
:
pass
更好的方法
with ignored
(
OSError
)
:
os
.
remove
(
"somefile.tmp"
)
ignored是Python 3.4加入的, 文檔。
注意:ignored 實際上在標準庫叫suppress(譯註:contextlib.supress).
試試創建你自己的ignored上下文管理器。
@
contextmanager
def ignored
(
*
exceptions
)
:
try
:
yield
except
exceptions
:
pass
把它放在你的工具目錄,你也可以忽略異常
譯註:contextmanager在標準庫contextlib中,通過裝飾生成器函數,省去用__enter__和__exit__寫上下文管理器。詳情請看文檔。
分離臨時上下文
# 臨時把標準輸出重定向到一個文件,然後再恢復正常
with open
(
"help.txt"
,
"w"
)
as
f
:
oldstdout
=
sys
.
stdout
sys
.
stdout
=
f
try
:
help
(
pow
)
finally
:
sys
.
stdout
=
oldstdout
更好的寫法
with open
(
"help.txt"
,
"w"
)
as
f
:
with redirect_stdout
(
f
)
:
help
(
pow
)
redirect_stdout在Python 3.4加入(譯註:contextlib.redirect_stdout), bug反饋。
實現你自己的redirect_stdout上下文管理器。
@
contextmanager
def redirect_stdout
(
fileobj
)
:
oldstdout
=
sys
.
stdout
sys
.
stdout
=
fileobj
try
:
yield fieldobj
finally
:
sys
.
stdout
=
oldstdout
簡潔的單句表達
兩個衝突的原則:
一行不要有太多邏輯
不要把單一的想法拆分成多個部分
Raymond的原則:
一行代碼的邏輯等價於一句自然語言
列表解析和生成器
result
=
[]
for
i
in
range
(
10
)
:
s
=
i *
*
2
result
.
append
(
s
)
print sum
(
result
)
更好的方法
print sum(i**2 for i in xrange(10))
第一種方法說的是你在做什麼,第二種方法說的是你想要什麼。
看完本文有收穫?請轉
發分享給更多人
關注「P
ython開發者」,提升Python技能
※一個 Reentrant Error 引發的對 Python 信號機制的探索和思考
※初探 Python 3 的非同步 IO 編程
※為提高用戶體驗,Yelp 是如何無損壓縮圖片的
※BAT 面試官帶你刷真題、過筆試
※K-means 在 Python 中的實現
TAG:Python開發者 |