让你的 Python 代码优雅又地道

在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 **2foriinrange(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):return1return0print sorted(colors,cmp=compare_length)

更好的方法

print sorted(colors, key=len)

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

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

blocks=[]whileTrue:block=f.read(32)ifblock=='':breakblocks.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=Falsefori,valueinenumerate(seq):ifvalue==target:found=Truebreakifnotfound:return-1returni

更好的方法

def find(seq,target):fori,valueinenumerate(seq):ifvalue==target:breakelse:return-1returni

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:printkforkind.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]=0d[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返回值, 一个namedtupledoctest.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=0y=1foriinrange(n):printxt=yy=x+yx=t

更好的方法

def fibonacci(n):x,y=0,1foriinrange(n):printxx,y=y,x+y

第一种方法的问题

x和y是状态,状态应该在一次操作中更新,分几行的话状态会互相对不上,这经常是bug的源头。

操作有顺序要求

太底层太细节

第二种方法抽象层级更高,没有操作顺序出错的风险而且更效率更高。

同时状态更新

tmp_x=x+dx *ttmp_y=y+dy *ttmp_dx=influence(m,x,y,dx,dy,partial='x')tmp_dy=influence(m,x,y,dx,dy,partial='y')x=tmp_xy=tmp_ydx=tmp_dxdy=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+=', '+nameprints

更好的方法

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]=pagereturnpage

更好的方法

@cachedef web_lookup(url):returnurllib.urlopen(url).read()

注意:Python 3.2开始加入了functools.lru_cache解决这个问题。

分离临时上下文

# 保存旧的,创建新的old_context=getcontext().copy()getcontext().prec=50print 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上下文管理器。

@contextmanagerdef ignored(*exceptions):try:yieldexceptexceptions:pass

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

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

分离临时上下文

# 临时把标准输出重定向到一个文件,然后再恢复正常with open('help.txt','w')asf:oldstdout=sys.stdoutsys.stdout=ftry: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.stdoutsys.stdout=fileobjtry:yield fieldobjfinally:sys.stdout=oldstdout

简洁的单句表达

两个冲突的原则:

一行不要有太多逻辑

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

Raymond的原则:

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

列表解析和生成器

result=[]foriinrange(10):s=i **2result.append(s)print sum(result)

更好的方法

print sum(i**2 for i in xrange(10))

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

原文发布于微信公众号 - 顶级程序员(TopCoding)

原文发表时间:2017-09-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户1191492的专栏

物联网平台设计文档:精简GC(垃圾回收)

许多高级编程语言的自动内存管理功能让编程变成了比较容易的一件事。然而,嵌入式平台经常缺少这一部分功能,这是有原因的:现代垃圾收集(GC)系统使用的...

2955
来自专栏鸿的学习笔记

合理选择数据结构

写程序很重要的一点是选择合理的数据结构,不合适的数据结构在如今高性能计算机盛行的情况下,小数据量体现不出什么来,但是在超大数据的时候, 你所面临的困境将会无穷的...

792
来自专栏磐创AI技术团队的专栏

干货 | 如何写一个更好的Python函数?

《Writing Idiomatic Python》一书的作者在Medium上发表了一篇文章,给出了6个建议。

801
来自专栏PHP在线

PHP程序员容易忽略的几点精华

1、变量、数组的应用技巧   (1)很多人用得不多的数组函数。foreach、list、each。分别举几个例子,应该就能知道了。例:   $dat...

37110
来自专栏lgp20151222

Java的常量接口思考,项目中的常量是放在接口里还是放在类里呢?

最近在看一本书 Java与模式,里面提了一句不建议使用常量接口,甚至举了个java源码的反例,

1371
来自专栏编程

手把手教你半个小时用python语言编程出你的第一个程序

学习目标 知道有序的软件开发过程的步骤。 了解遵循输入、处理、输出(IPO)模式的程序,并能够以简单的方式修改它们。 了解构成有效Python标识符和表达式的规...

4065
来自专栏灯塔大数据

技术 | Python从零开始系列连载(二十七)

为了解答大家学习Python时遇到各种常见问题,小灯塔特地整理了一系列从零开始的入门到熟练的系列连载,每周五准时推出,欢迎大家学积极学习转载~

1183
来自专栏北京马哥教育

让你的 Python 代码优雅又地道

学Python最简单的方法是什么?推荐阅读:Python开发工程师成长魔法 译序 如果说优雅也有缺点的话,那就是你需要艰巨的工作才能得到它,需要良好的教育才能欣...

40310
来自专栏企鹅号快讯

30分钟学会用Python编写简单程序

参与文末每日话题讨论,赠送异步新书 异步图书君 学习目标 知道有序的软件开发过程的步骤。 了解遵循输入、处理、输出(IPO)模式的程序,并能够以简单的方式修改它...

66810
来自专栏皮皮之路

【JDK1.8】Java 8源码阅读汇总

3947

扫码关注云+社区

领取腾讯云代金券