當前位置:
首頁 > 知識 > 让你的 Python 代码优雅又地道

让你的 Python 代码优雅又地道

点击上方“

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的演讲。这是我按我的理解整理出来的,希望你们理解起来跟我一样顺畅!


遍历一个范围内的数字

foriin[

0

,

1

,

2

,

3

,

4

,

5

]:
printi **

2

foriinrange(

6

):
printi **

2


更好的方法

foriinxrange(

6

):
printi **

2


xrange会返回一个迭代器,用来一次一个值地遍历一个范围。这种方式会比range更省内存。xrange在Python 3中已经改名为range。


遍历一个集合

colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]
foriinrange(len(colors)):
printcolors[i]

更好的方法

forcolorincolors:
printcolor

反向遍历

colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]
foriinrange(len(colors)-

1

,-

1

,-

1

):
printcolors[i]

更好的方法

forcolorinreversed(colors):
printcolor

遍历一个集合及其下标

colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]
foriinrange(len(colors)):
printi,

"--->"

,colors[i]

更好的方法

fori,colorinenumerate(colors):printi,

"--->"

,color


这种写法效率高,优雅,而且帮你省去亲自创建和自增下标。




当你发现你在操作集合的下标时,你很有可能在做错事。





遍历两个集合


names=[

"raymond"

,

"rachel"

,

"matthew"

]
colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]
n=min(len(names),len(colors))
foriinrange(n):
printnames[i],

"--->"

,colors[i]
forname,colorinzip(names,colors):
printname,

"--->"

,color

更好的方法

forname,colorinizip(names,colors):printname,

"--->"

,color

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


注意:在Python 3中,izip改名为zip,并替换了原来的zip成为内置函数。


有序地遍历

colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]
# 正序
forcolorinsorted(colors):
printcolors
# 倒序
forcolorinsorted(colors,reverse=True):
printcolors

自定义排序顺序

colors=[

"red"

,

"green"

,

"blue"

,

"yellow"

]def compare_length(c1,c2):
iflen(c1)
iflen(c1)>len(c2):return1
return0
print sorted(colors,cmp=compare_length)

更好的方法

print sorted(colors, key=len)

第一种方法效率低而且写起来很不爽。另外,Python 3已经不支持比较函数了。


调用一个函数直到遇到标记值

blocks=[]
whileTrue:
block=f.read(

32

)
ifblock==

""

:

break

blocks.append(block)

更好的方法

blocks=[]
forblockiniter(partial(f.read,

32

),

""

):
blocks.append(block)

iter接受两个参数。第一个是你反复调用的函数,第二个是标记值。


译注:这个例子里不太能看出来方法二的优势,甚至觉得partial让代码可读性更差了。方法二的优势在于iter的返回值是个迭代器,迭代器能用在各种地方,set,sorted,min,max,heapq,sum……


在循环内识别多个退出点

def find(seq,target):
found=False
fori,valueinenumerate(seq):
ifvalue==target:
found=True

break

ifnotfound:

return

-

1

returni

更好的方法

def find(seq,target):fori,valueinenumerate(seq):
ifvalue==target:

breakelse

:

return

-

1

returni

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"

}forkind:
printk
forkind.keys():
ifk.startswith(

"r"

):
deld[k]

什么时候应该使用第二种而不是第一种方法?当你需要修改字典的时候。



如果你在迭代一个东西的时候修改它,那就是在冒天下之大不韪,接下来发生什么都活该。




d.keys()把字典里所有的key都复制到一个列表里。然后你就可以修改字典了。



注意:如果在Python 3里迭代一个字典你得显示地写:list(d.keys()),因为d.keys()返回的是一个“字典视图”(一个提供字典key的动态视图的迭代器)。详情请看文档。


遍历一个字典的key和value

# 并不快,每次必须要重新哈希并做一次查找
forkind:printk,

"--->"

,d[k]
# 产生一个很大的列表
fork,vind.items():
printk,

"--->"

,v

更好的方法

fork,vind.iteritems():printk,

"--->"

,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={}
forcolorincolors:
ifcolornotind:
d[color]=

0

d[color]+=

1

# {

"blue"

:

1

,

"green"

:

2

,

"red"

:

3

}

更好的方法

d={}
forcolorincolors:
d[color]=d.get(color,

0

)+

1

# 稍微潮点的方法,但有些坑需要注意,适合熟练的老手。
d=defaultdict(int)
forcolorincolors:
d[color]+=

1


用字典分组 — 第I部分和第II部分

names=[

"raymond"

,

"rachel"

,

"matthew"

,

"roger"

,

"betty"

,

"melissa"

,

"judith"

,

"charlie"

]
# 在这个例子,我们按name的长度分组
d={}
fornameinnames:
key=len(name)
ifkeynotind:
d[key]=[]
d[key].append(name)
# {

5

: [

"roger"

,

"betty"

],

6

: [

"rachel"

,

"judith"

],

7

: [

"raymond"

,

"matthew"

,

"melissa"

,

"charlie"

]}
d={}
fornameinnames:
key=len(name)
d.setdefault(key,[]).append(name)

更好的方法

d=defaultdict(list)fornameinnames:
key=len(name)
d[key].append(name)

字典的popitem()是原子的吗?

d={

"matthew"

:

"blue"

,

"rachel"

:

"green"

,

"raymond"

:

"red"

}
whiled:
key,value=d.popitem()
printkey,

"-->"

,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:vfork,vinvars(namespace).items()ifv}
# 下面是通常的作法,默认使用第一个字典,接着用环境变量覆盖它,最后用命令行参数覆盖它。
# 然而不幸的是,这种方法拷贝数据太疯狂
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

foriinrange(n):
printx
t=y
y=x+y
x=t

更好的方法

def fibonacci(n):x,y=

0

,

1

foriinrange(n):
printx
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

]
fornameinnames[

1

:]:
s+=

", "

+name
prints

更好的方法

print

", "

.join(names)

更新序列

names=[

"raymond"

,

"rachel"

,

"matthew"

,

"roger"

,

"betty"

,

"melissa"

,

"judith"

,

"charlie"

]
delnames[

0

]
# 下面的代码标志着你用错了数据结构
names.pop(

0

)
names.insert(

0

,

"mark"

)

更好的方法

names=deque([

"raymond"

,

"rachel"

,

"matthew"

,

"roger"

,

"betty"

,

"melissa"

,

"judith"

,

"charlie"

])
# 用deque更有效率
delnames[

0

]
names.popleft()
names.appendleft(

"mark"

)

装饰器和上下文管理


用于把业务和管理的逻辑分开


分解代码和提高代码重用性的干净优雅的好工具


起个好名字很关键


记住蜘蛛侠的格言:能力越大,责任越大


使用装饰器分离出管理逻辑

# 混着业务和管理逻辑,无法重用def web_lookup(url,saved={}):
ifurlinsaved:
returnsaved[url]
page=urllib.urlopen(url).read()
saved[url]=page
returnpage

更好的方法

@cachedef web_lookup(url):
returnurllib.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"

)asf:data=f.read()

如何使用锁

# 创建锁
lock=threading.Lock()
# 使用锁的老方法
lock.acquire()

try

:
print

"Critical section 1"

print

"Critical section 2"

finally

:
lock.release()

更好的方法

# 使用锁的新方法withlock:
print

"Critical section 1"

print

"Critical section 2"


分离出临时的上下文

try

:
os.remove(

"somefile.tmp"

)
exceptOSError:
pass

更好的方法

with

ignored
(OSError):os.remove(

"somefile.tmp"

)

ignored是Python 3.4加入的, 文档。


注意:ignored 实际上在标准库叫suppress(译注:contextlib.supress).


试试创建你自己的ignored上下文管理器。

@contextmanager
def ignored(*exceptions):

try

:

yield

exceptexceptions:
pass


把它放在你的工具目录,你也可以忽略异常




译注:contextmanager在标准库contextlib中,通过装饰生成器函数,省去用enter和exit写上下文管理器。详情请看文档。



分离临时上下文

# 临时把标准输出重定向到一个文件,然后再恢复正常

with

open(

"help.txt"

,

"w"

)asf:
oldstdout=sys.stdout
sys.stdout=f

try

:
help(pow)

finally

:
sys.stdout=oldstdout

更好的写法

with

open
(

"help.txt"

,

"w"

)asf:

with

redirect_stdout(f):
help(pow)

redirect_stdout在Python 3.4加入(译注:contextlib.redirect_stdout), bug反馈。


实现你自己的redirect_stdout上下文管理器。

@contextmanagerdef redirect_stdout(fileobj):
oldstdout=sys.stdout
sys.stdout=fileobj

try

:

yield

fieldobj

finally

:
sys.stdout=oldstdout

简洁的单句表达


两个冲突的原则:


一行不要有太多逻辑


不要把单一的想法拆分成多个部分


Raymond的原则:


一行代码的逻辑等价于一句自然语言


列表解析和生成器

result=[]
foriinrange(

10

):
s=i **

2

result.append(s)
print sum(result)

更好的方法

print sum(i**

2

for

i

in

xrange(

10

))

第一种方法说的是你在做什么,第二种方法说的是你想要什么。




  • 来源:

    Jeff Paine



  • www.lightxue.com/transforming-code-into-beautiful-idiomatic-python



  • Python开发整理发布,转载请联系作者获得授权




【点击成为安卓大神】

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

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


請您繼續閱讀更多來自 Python开发 的精彩文章:

Python 爬虫实践:《战狼2》豆瓣影评分析
帮你提升 Python 的 27 种编程语言

TAG:Python开发 |