Python 语法最佳实践
无论初学者还是非常有经验的工程师,或多或少都听过 Python 之禅(https://www.python.org/dev/peps/pep-0020/ )。笔者在初学 Python 的时候也曾经通过“import this”看过它的内容,但是由于它的语句非常让人费解而没有留下太多印象。直到笔者使用 Python 4 年多之后再去翻读,才渐渐领会到这些语句想要传达的 Python 的设计哲学。
开始讲述本节内容之前,我们很有必要把它列出来复习一遍,并逐行解读:
优美胜于丑陋。 无论什么级别的工程师都应该总是寻找更好的实现方式,如本节介绍的一些实践。除此之外,经常读一些优秀的开源项目和 Python 标准库,也可以帮你快速提升写 Python 代码的品味。
明了胜于晦涩。 大概每个人都曾经在代码中炫过技。当学会了一个新的知识,笔者也曾恨不得马上应用到自己的代码里面,有时候甚至会为了使用这些技巧而写。炫技的结果就是代码晦涩难懂,其他维护者或者一段时间之后你自己再来看这段代码就会需要花费更多的时间才能理解它要表达的意图。
简洁胜于复杂。 要在合适的地方使用最合适的技术,用最简洁的代码实现功能即可。
复杂胜于凌乱。 如果复杂不可避免,但是仍然可以在实现功能的时候让代码结构和接口明确,各模块之前关系有层次感,利于阅读和维护。
扁平胜于嵌套。 不要使用太多的嵌套,嵌套太深不利于阅读和理解。
间隔胜于紧凑。 不要妄图一行代码实现功能,整段代码没有空行作为间隔,堆在一起。适量的空行使代码逻辑更清晰,应该适当地把功能的不同部分用空格分隔开。
可读性很重要。 代码是给人读的,顺便让机器执行。除了让代码实现得更简洁明了,命名也很重要,下面我们会讲到一些提高可读性的原则。
特例并没有有特殊到可以违背规则的地步。 Python 相对于其他语言有比较多的限制,很多人懂 Python 代码之美,也有很多人觉得代码能运行就好了,至于怎么组织代码,怎么提升运行效率,怎么写得更 Pythonic,他们并不关心。笔者也会遇到某些特例,比如一句话放在一行超过 79 个字符,拆分成 2 行又很奇怪。笔者通常为了可读性选择放在一行,但是在行尾添加“# noqa”标识,保证不让测试失败,这并没有完全遵守 PEP8。最怕的是没有意识,完全不考虑阅读和维护这段代码的其他人的感受。
虽然适用性胜于纯粹性,也不该默默地忽略错误,除非明确地忽略。 之前强调了不要捕获全部错误,也不要完全忽略。下面这样的代码没有任何意义,应该尽量避免:
try: xxx except: pass
当存在多种可能时,不要尝试去猜测,尽量找一种,最好只有一种明显的解决方案,虽然这并不如意,因为你不是 Python 之父。 编程的乐趣之一是可以使用多种方式实现同样的功能,但是往往最优解只有一个。在学习 Python 的路上,这个最优解会随着你对 Python 的熟悉而改变,前提是你愿意花时间去思考当前所能使用的最好的方式是不是那个最优解,有没有更好的解决方案?
做也许好过不做,但盲目动手还不如不做。 写代码之前要明确需求,考虑清楚怎么组织代码,实现的难点有哪些等,避免花了很多精力实现一半的时候才发现其实一开始的方向就有问题,甚至需要推翻重来。
如果方案很难解释,那就不是最好的方案。 复杂的方案往往意味着逻辑复杂,需要花费更多的时间和资源。
如果方案容易解释,那么有可能这是一个好方案。 容易解释,说明开发者思路清晰,而思路清晰在方案评审的时候也可以让其他人更容易理解,更好地评判方案的可行性。
命名空间是一种绝妙的理念,我们应当多加利用。 合理利用命名空间,减少隐式的命名冲突。
Python 有一些惯用写法,Pythonic 的写法相对更简练、明确、优雅,绝大部分时候执行效率更高,而且代码越少也就越不容易出错。如下资料提供了一些写出优美的 Pythonic 代码的实践:
- sloria 整理的 Python 最佳实践(http://bit.ly/1PwdxH5 )
- JeffPaine 整理的 Idiomatic Python(http://bit.ly/265xmPd )
每个工程师都应该熟练掌握上述文档。
命名
统一的命名风格可以让项目具有更高的可读性。
1.变量、函数、方法和包名使用全小写字母和下画线的组合,如 lower_case_with_underscores。
2.类和异常使用大写字母开头的单词,如 CapWords。
3.受保护的或者内部方法可以使用一个下画线开头,如_single_leading_underscore(self, ...)。
4.私有方法使用两个下画线开头,如__double_leading_underscore(self, ...)。
5.常量使用全大写的字母和下画线的组合,如 ALL_CAPS_WITH_UNDERSCORES。
6.尽量不要使用单个字符的变量,除非是上下文非常清晰(如在列表解析中作为中间变量)的情况。
7.避免多余的标签。如果类或者包的名字已经包含对应的单词,那么对应的属性或者方法上就不用出现了。举个例子:
import audio core=audio.AudioCore() controller=audio.AudioController()
可以简化为:
import audio core=audio.Core() controller=audio.Controller()
8.动词和形容词放在名词之后。如使用:
elements=... elements_active=... elements_defunct=...
而不要把它们放在名词前面:
elements= ... active_elements= ... defunct_elements ...
9.使用更容易理解的动作,比如设置属性,使用“user.xxx=23”,而不要使用“user.set_xxx(23)”。
使用 join 连接字符串
常见的性能不高且代码不够优雅的使用方法是这样的:
In : my_list=['X', 'y', 'p', 'J', 'F', 'r'] In : rs='' In : for c in my_list: ....: rs+=c+' ' ....: In : rs=rs.strip() In : rs Out: 'X y p J F r'
正确的用法是先创建一个列表,不断地 append,最后使用 join 连接:
In : rs=[] In : for c in my_list: ....: rs.append(c) ....: In : ' '.join(rs) Out: 'X y p J F r'
当然,对于这个例子直接使用' '.join(my_list) 就好了。当合并的元素比较少时,使用 join 方法看不出太大的效果,但是当元素多的时候,就会发现 join 的效率优势还是非常明显的。
EAFP vs LBYL
检查数据可以让程序更健壮,这就是所谓的防御性编程。数据检查有 EAFP 和 LBYL 两种编程风格。
1.LBYL:Look Before You Leap,即事先检查,通过使用 if 语句把错误输入转化成合理的用法或者返回错误信息。比如单词计数功能:
In : d={} In : words=['a', 'b', 'c', 'a', 'a'] In : for w in words: ...: if w not in d: ...: d[w]=1 ...: else: ...: d[w]+=1 ...:
2.EAFP:Easier to Ask Forgiveness than Permission,即不检查,出了问题由异常处理来处理。
In : for w in words: ...: try: ...: d[w]+=1 ...: except KeyError: ...: d[w]=1 ...:
一般情况下,Python 程序应该倾向使用 EAFP 风格。尤其涉及原子操作时,一定要使用 EAFP 风格,比如在多线(进)程并发的时候,可能要操作的对象已经被其他线(进)程改变了。
当然,也不是都要使用 EAFP 风格,如果你预期操作在逻辑上失败的概率超过 50%,就应该使用 LBYL。
定义类的__str__/__repr__方法
自定义类的默认的__str__方法的实现是无意义的:
In : class Board(object): ...: def__init__(self, id, name): ...: self.id=id ...: self.name=name ...: In : board=Board(1, '最新热评榜单') In : board Out: <__main__.Board at 0x7f735dbd6ac8>
好的习惯是自定义__str__和__repr__方法,提供有价值的输出:
In : class Board(object): ...: def __init__(self, id, name): ...: self.id=id ...: self.name=name ...: def __str__(self): ...: return '({},{})'.format(self.id, self.name) ...: def __repr__(self): ...: return '{}(id={}, name={})'.format( ...: self.__class__.__name__, ...: self.id, self.name) ...: In : board=Board(1, '最新热评榜单') In : board Out: Board(id=1, name=最新热评榜单) In : print board (1, 最新热评榜单)
优美的 Python
下面将看到上述文档中提到的几个鲜为人知的例子,以及一些笔者在工作上的实践。
使用 next 获取循环中符合条件的值
通常,要从一个循环中找到符合条件的值,会使用如下方法:
a=-1 for i in range(1, 10): if not i % 4: a=i break
此时 a 等于 4。而更好的写法是使用 next:
a=next((i for i in range(1, 10) if not i%4),-1)
执行调用直到某种情况结束
使用如下方式读取文件:
blocks=[] while True: block=f.read(32) if block=='': break blocks.append(block)
而更好的写法是:
from functools import partial blocks=[] for block in iter(partial(f.read, 32), ''): blocks.append(block)
善用 for...else 句式
常见的寻找符合条件的序列的索引值的方法如下:
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
第二种方法不用引入 found 这个变量。
最简单的缓存实现
最常见的实现缓存的方法如下:
def web_lookup(url, saved={}): if url in saved: return saved[url] page=urllib.urlopen(url).read() saved[url]=page return page
这样的缓存用法比较隐晦,而且灵活性不足,可以简单地改写成装饰器风格的缓存:
def cache(func): saved={} @wraps(func) def newfunc(*args): if args in saved: return saved[args] result=func(*args) saved[args]=result return result return newfunc @cache def web_lookup(url): return urllib.urlopen(url).read()
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论