译序

 

如果说优雅也有缺点的话,那就是你需要艰巨的工作才能得到它,需要良好的教育才能欣赏它。

 

—— 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))

 

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

 

编译: 0xFEE1C001 

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

来源: Python 开发者