联系电话:0411-66373325
联系地址:大连市沙河口区富民广场
公司邮箱:2058793689@qq.com
备案信息:Copyright © 2016-2025,www.my249.com,All rights reserved
|
249建站之家:如何写出优雅又地道的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中,通过装饰生成器函数,省去用__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)) 第一种方法说的是你在做什么,第二种方法说的是你想要什么。 |