让你的Python代码优雅又地道

简介: 译序如果说优雅也有缺点的话,那就是你需要艰巨的工作才能得到它,需要良好的教育才能欣赏它。—— Edsger Wybe Dijkstra在Python社区文化的浇灌下,演化出了一种独特的代码风格,去指导如何正确地使用Python,这就是常说的pythonic。

译序

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

—— 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开发者

目录
相关文章
|
12天前
|
Python
以下是一些常用的图表类型及其Python代码示例,使用Matplotlib和Seaborn库。
以下是一些常用的图表类型及其Python代码示例,使用Matplotlib和Seaborn库。
|
15天前
|
Python
turtle库的几个案例进阶,代码可直接运行(python经典编程案例)
该文章展示了使用Python的turtle库进行绘图的进阶案例,包括绘制彩色圆形和复杂图案的代码示例。
73 6
turtle库的几个案例进阶,代码可直接运行(python经典编程案例)
|
7天前
|
数据安全/隐私保护 Python
探索Python中的装饰器:简化代码,提升效率
【9月更文挑战第32天】在Python编程世界中,装饰器是一个强大的工具,它允许我们在不改变函数源代码的情况下增加函数的功能。本文将通过直观的例子和代码片段,引导你理解装饰器的概念、使用方法及其背后的魔法,旨在帮助你写出更加优雅且高效的代码。
|
1天前
|
缓存 测试技术 开发者
探索Python中的装饰器:提升代码的灵活性和可维护性
在Python编程中,装饰器是一种强大且灵活的工具,它允许开发者在不修改现有代码的基础上,为函数或类添加额外的功能。本文将深入探讨装饰器的定义、使用场景以及如何创建自定义装饰器。通过实用的示例,我们将展示如何利用装饰器来增强代码的可重用性和可读性。
|
1天前
|
小程序 iOS开发 MacOS
将Python代码转化为可执行的程序
将Python代码转化为可执行的程序
10 1
|
1天前
|
存储 人工智能 开发工具
AI助理化繁为简,速取代码参数——使用python SDK 处理OSS存储的图片
只需要通过向AI助理提问的方式输入您的需求,即可瞬间获得核心流程代码及参数,缩短学习路径、提升开发效率。
49 0
AI助理化繁为简,速取代码参数——使用python SDK 处理OSS存储的图片
|
2天前
|
缓存 数据安全/隐私保护 开发者
探索Python中的装饰器:提升代码效率与可读性
在本文中,我们将深入探讨Python中的装饰器——一种能够修改或增强函数行为的强大工具。通过详细讲解装饰器的定义、使用方法以及实际案例分析,我们希望能够帮助读者更好地理解和应用这一技术,从而编写出更加高效和易读的代码。无论是初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧。
|
6天前
|
大数据 Python
Python 高级编程:深入探索高级代码实践
本文深入探讨了Python的四大高级特性:装饰器、生成器、上下文管理器及并发与并行编程。通过装饰器,我们能够在不改动原函数的基础上增添功能;生成器允许按需生成值,优化处理大数据;上下文管理器确保资源被妥善管理和释放;多线程等技术则助力高效完成并发任务。本文通过具体代码实例详细解析这些特性的应用方法,帮助读者提升Python编程水平。
29 5
|
3天前
|
设计模式 缓存 测试技术
探索Python中的装饰器:提升代码效率与可读性
本文深入探讨Python装饰器的概念、使用方法和实际应用场景。通过详细讲解和示例,读者将学会如何利用装饰器简化代码、增强功能模块的可重用性,并提高开发效率。无论是初学者还是经验丰富的开发者,都能从中获得有价值的见解和实用的编程技巧。
10 1
|
11天前
|
Python
? Python 装饰器入门:让代码更灵活和可维护
? Python 装饰器入门:让代码更灵活和可维护
15 4