當前位置:
首頁 > 知識 > 讓你的 Python 代碼優雅又地道

讓你的 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

]

:


    

print

i *

*

2


 


for

i

in

range

(

6

)

:


    

print

i *

*

2




更好的方法





for

i

in

xrange

(

6

)

:


    

print

i *

*

2




xrange會返回一個迭代器,用來一次一個值地遍歷一個範圍。這種方式會比range更省內存。xrange在Python 3中已經改名為range。




遍歷一個集合





colors

=

[

"red"

,

"green"

,

"blue"

,

"yellow"

]


 


for

i

in

range

(

len

(

colors

))

:


    

print

colors

[

i

]




更好的方法





for

color

in

colors

:


    

print

color




反向遍歷





colors

=

[

"red"

,

"green"

,

"blue"

,

"yellow"

]


 


for

i

in

range

(

len

(

colors

)

-

1

,

-

1

,

-

1

)

:


    

print

colors

[

i

]




更好的方法





for

color

in

reversed

(

colors

)

:


    

print

color




遍歷一個集合及其下標





colors

=

[

"red"

,

"green"

,

"blue"

,

"yellow"

]


 


for

i

in

range

(

len

(

colors

))

:


    

print

i

,

"--->"

,

colors

[

i

]




更好的方法





for

i

,

color

in

enumerate

(

colors

)

:


    

print

i

,

"--->"

,

color





這種寫法效率高,優雅,而且幫你省去親自創建和自增下標。




當你發現你在操作集合的下標時,你很有可能在做錯事。






遍歷兩個集合





names

=

[

"raymond"

,

"rachel"

,

"matthew"

]


colors

=

[

"red"

,

"green"

,

"blue"

,

"yellow"

]


 


n

=

min

(

len

(

names

),

len

(

colors

))


for

i

in

range

(

n

)

:


    

print

names

[

i

],

"--->"

,

colors

[

i

]


 


for

name

,

color

in

zip

(

names

,

colors

)

:


    

print

name

,

"--->"

,

color




更好的方法





for

name

,

color

in

izip

(

names

,

colors

)

:


    

print

name

,

"--->"

,

color




zip在內存中生成一個新的列表,需要更多的內存。izip比zip效率更高。




注意:在Python 3中,izip改名為zip,並替換了原來的zip成為內置函數。




有序地遍歷





colors

=

[

"red"

,

"green"

,

"blue"

,

"yellow"

]


 


# 正序


for

color

in

sorted

(

colors

)

:


    

print

colors


 


# 倒序


for

color

in

sorted

(

colors

,

reverse

=

True

)

:


    

print

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

:


    

print

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

:


    

print

k

,

"--->"

,

d

[

k

]


 


# 產生一個很大的列表


for

k

,

v

in

d

.

items

()

:


    

print

k

,

"--->"

,

v




更好的方法





for

k

,

v

in

d

.

iteritems

()

:


    

print

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

()


    

print

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

]


email

=

p

[

3

]




更好的方法





fname, lname, age, email = p




第二種方法用了unpack元組,更快,可讀性更好。




更新多個變數的狀態





def fibonacci

(

n

)

:


    

x

=

0


    

y

=

1


    

for

i

in

range

(

n

)

:


        

print

x


        

t

=

y


        

y

=

x

+

y


        

x

=

t




更好的方法





def fibonacci

(

n

)

:


    

x

,

y

=

0

,

1


    

for

i

in

range

(

n

)

:


        

print

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

_

x


y

=

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


print

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

:


    

print

"Critical section 1"


    

print

"Critical section 2"


finally

:


    

lock

.

release

()




更好的方法





# 使用鎖的新方法


with

lock

:


    

print

"Critical section 1"


    

print

"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技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

一個 Reentrant Error 引發的對 Python 信號機制的探索和思考
初探 Python 3 的非同步 IO 編程
為提高用戶體驗,Yelp 是如何無損壓縮圖片的
BAT 面試官帶你刷真題、過筆試
K-means 在 Python 中的實現

TAG:Python開發者 |