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

遍历一个范围内的数字

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 中,通过装饰生成器函数,省去用enterexit写上下文管理器。详情请看文档。

分离临时上下文

# 临时把标准输出重定向到一个文件,然后再恢复正常
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))
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容