如果有人问起 Python 程序员他们最喜欢 Python 哪一点,他们一定会提到 Python 的高可读性。确实,对于 Python 来说,其高可读性一直是 Python 这门语言设计的核心。一个不争的事实是,相对于写代码而言,读代码才是更加平常的事情。
Python 代码有高可读性的一个原因就是其有着相对而言更加完善的编码风格准则和 「Python化」习语。
当 Python 老手(Pythonista)认为一段代码不「Python化」,他们通常的意思是这段代码没有遵循一般准则,同时亦没有以最佳的(最具可读性的)方式表达出代码的意图。
在一些极端的情况下,没有公认最佳的方式来表达 Python 代码的意图,不过这种极端情况非常罕见。
一般概念
明确代码意义
尽管 Python 可以写出从各种意义上来说都像是黑魔法的代码,但最简单直白的表达才是正道。
不好
1 | def make_complex(*args): |
好
1 | def make_complex(x, y): |
在上述好的代码中,x 和 y 清晰明了的从参数中获取值,并清晰明了的返回了一个字典。当开发者看到这个函数后就可以明了这个函数的用途,而不好的代码则不行。
一行一个声明语句
虽然在 Python 中我们推崇使用形如列表生成式这种简洁明了的复合语句,但是除此以外,我们应该尽量避免将两句独立分割的代码写在同一行。
不好的风格
1 | print 'one'; print 'two' |
好的风格
1 | print 'one' |
函数的参数
函数的参数可以使用四种不同的方式传递给函数。
- 必选参数 是没有默认值的必填的参数。 必选参数是最简单的参数构成,用于参数较少的函数的构成,是该函数意义的一部分,使用他们的顺序是按照定义自然排序的。举个例子,对于
send(message, recipient)
和point(x, y)
这两个函数,使用函数的人需要知道这个函数需要两个参数,并且记住两个参数的顺序。
在调用函数的时候,我们也可以使用参数的名称调用。使用参数的名称的方式可以调换参数的顺序,就像 send(recipient='World',message='Hello')
和 point(y=2, x=1)
这样。但这样的做法会降低代码的可读性,并且使代码冗长,因此更建议使用 send('Hello', 'World')
和point(1,2)
这样的方式调用。
- 关键字参数 是非强制的,且有默认值。它们经常被用在传递给函数的可选参数中。 当一个函数有超过两个或三个位置参数时,函数签名会变得难以记忆,使用带有默认参数的关键字参数有时候会给你带来便利。比如,一个更完整的
send
函数可以被定义为send(message, to, cc=None, bcc=None)
。这里的cc
和bcc
是可选的, 当没有传递给它们其他值的时候,它们的值就是 None。
Python 中有多种方式调用带关键字参数的函数。比如说,我们可以按照定义时的参数顺序而无需明确的命名参数来调用函数,就像 send('Hello', 'World', 'Cthulhu', 'God')
是将密件发送给上帝。我们也可以使用命名参数而无需遵循参数顺序来调用函数,就像send('Hello again', 'World', bcc='God', cc='Cthulhu')
。没有特殊情况的话,这两种方式都需要尽力避免,最优的调用方式是与定义方式一致:send('Hello', 'World', cc='Cthulhu',bcc='God')
。
作为附注,请遵循 YAGNI 原则。 通常,移除一个用作『以防万一』但从未使用的可选参数(以及它在函数中的逻辑),比添加一个所需的新的可选参数和它的逻辑要来的困难。
- 任意参数列表 是第三种给函数传参的方式。如果函数的参数数量是动态的,该函数可以被定义成
*args
的结构。在这个函数体中,args
是一个元组,它包含所有剩余的位置参数。举个例子, 我们可以用任何容器作为参数去调用send(message, *args)
,比如send('Hello', 'God', 'Mom','Cthulhu')
。 在此函数体中,args
相当于('God','Mom', 'Cthulhu')
。
然而,这种结构有一些缺点,使用时应该特别注意。如果一个函数接受的参数列表具有相同的性质,通常把它定义成一个参数,这个参数是一个列表或者其他任何序列会更清晰。 在这里,如果 send
参数有多个容器(recipients),将之定义成 send(message,recipients)
会更明确,调用它时就使用 send('Hello', ['God', 'Mom', 'Cthulhu'])
。这样的话, 函数的使用者可以事先将容器列表维护成列表(list)形式,这为传递各种不能被转变成其他序列的序列(包括迭代器)带来了可能。
- 任意关键字参数字典 是最后一种给函数传参的方式。如果函数要求一系列待定的命名参数,我们可以使用
**kwargs
的结构。在函数体中,kwargs
是一个字典,它包含所有传递给函数但没有被其他关键字参数捕捉的命名参数。
和 任意参数列表 中所需注意的一样,相似的原因是:这些强大的技术在非特殊情况下,都要尽量避免使用,因为其缺乏简单和明确的结构来足够表达函数意图。
编写函数的时候采用何种参数形式,是用位置参数,还是可选关键字参数,是否使用形如任意参数 的高级技术,这些都由程序员自己决定。如果能明智地遵循上述建议,即可轻松写出这样的 Python 函数:
- 易读(名字和参数无需解释)
- 易改(添加新的关键字参数不会破坏代码的其他部分)
避免魔法方法
Python 对骇客来说是一个强有力的工具,它拥有非常丰富的钩子(hook)和工具,允许你施展几乎任何形式的技巧。比如说,它能够做以下:
- 改变对象创建和实例化的方式;
- 改变 Python 解释器导入模块的方式;
- 甚至可能(如果需要的话也是被推荐的)在 Python 中嵌入 C 程序。
尽管如此,所有的这些选择都有许多缺点。使用最直接的方式来达成目标通常是最好的方法。它们最主要的缺点是可读性不高。许多代码分析工具,比如说 pylint 或者 pyflakes,将无法解析这种『魔法』代码。
我们认为 Python 开发者应该知道这些近乎无限的可能性,因为它为我们灌输了没有不可能完成的任务的信心。然而,知道何时 不能 使用它们也是非常重要的。
就像一位功夫大师,一个 Pythonista 知道如何用一个手指杀死对方,但从不会那么去做。
我们都是负责任的用户
如前所述,Python 允许很多技巧,其中一些具有潜在的危险。一个好的例子是:任何客户端代码能够重写一个对象的属性和方法(Python 中没有 private
关键字)。这种哲学是在说:『我们都是负责任的用户』,它和高度防御性的语言(如 Java,拥有很多机制来预防错误操作)有着非常大的不同。
这并不意味着,比如说,Python 中没有属性是私有的,也不意味着没有合适的封装方法。 与其依赖在开发者的代码之间树立起的一道道隔墙,Python 社区更愿意依靠一组约定,来表明这些元素不应该被直接访问。
私有属性的主要约定和实现细节是在所有的 内部 变量前加一个下划线。如果客户端代码打破了这条规则并访问了带有下划线的变量,那么因内部代码的改变而出现的任何不当的行为或问题,都是客户端代码的责任。
鼓励大方地使用此约定:任何不开放给客户端代码使用的方法或属性,应该有一个下划线前缀。这将保证更好的职责划分以及更容易对已有代码进行修改。将一个私有属性公开化总是可能的,但是把一个公共属性私有化可能是一个更难的选择。
返回值
当一个函数变得复杂,在函数体中使用多返回值的语句并不少见。然而,为了保持函数的可读性,建议在函数体中避免使用返回多个有意义的值。
在函数中返回结果主要有两种情况:函数正常运行并返回它的结果,以及错误的情况,要么因为一个错误的输入参数,要么因为其他导致函数无法完成计算或任务的原因。
如果你在面对第二种情况时不想抛出异常,返回一个值(比如说 None 或 False )来表明函数无法正确运行,可能是需要的。在这种情况下,越早返回所发现的不正确上下文越好。 这将帮助扁平化函数的结构:我们假定在『因为错误而返回』的语句后的所有代码都能够满足函数主要结果运算。这种类型的多发挥结果,是有必要的。
然而,当一个函数在其正常运行过程中有多个主要出口点时,它会变得难以调试其返回结果,所以保持单个出口点可能会更好。这也将有助于提取某些代码路径,而且多个出口点很有可能意味着这里需要重构:
1 | def complex_function(a, b, c): |
习语(Idiom)
编程习语,说得简单些,就是写代码的 方式。编程习语的概念在 c2 和 Stack Overflow 上有详尽的讨论。
符合习语的 Python 代码通常被称为 Pythonic。
通常只有一种、而且最好只有一种明显的方式去编写代码。对 Python 初学者来说,无意识的情况下很少能写出习语式 Python 代码,所以应该有意识地去获取习语的书写方式。
如下有一些常见的Python习语:
解包(Unpacking)
如果你知道一个列表或者元组的长度,你可以将其解包并为它的元素取名。比如,enumerate()
会对 list 中的每个项提供包含两个元素的元组:
1 | for index, item in enumerate(some_list): |
你也能通过这种方式交换变量:
1 | a, b = b, a |
嵌套解包也能工作:
1 | a, (b, c) = 1, (2, 3) |
Python 3 提供了扩展解包的新方法在 PEP 3132 有介绍:
1 | a, *rest = [1, 2, 3] |
创建一个被忽略的变量
如果你需要赋值(比如,在 解包(Unpacking) )但不需要这个变量,请使用 __
:
1 | filename = 'foobar.txt' |
注意
许多 Python 风格指南建议使用单下划线的
_
而不是这里推荐的双下划线__
来标记废弃变量。问题是,_
常用在作为gettext()
函数的别名,也被用在交互式命令行中记录最后一次操作的值。相反,使用双下划线 十分清晰和方便,而且能够消除使用其他这些用例所带来的意外干扰的风险。
创建一个含 N 个对象的列表
使用 Python 列表中的 *
操作符:
1 | four_nones = [None] * 4 |
创建一个含 N 个列表的列表
因为列表是可变的,所以 *
操作符(如上)将会创建一个包含 N 个且指向 同一个 列表的列表,这可能不是你想用的。取而代之,请使用列表解析:
1 | four_lists = [[] for __ in xrange(4)] |
注意:在 Python 3 中使用 range()
而不是 xrange()
。
根据列表来创建字符串
创建字符串的一个常见习语是在空的字符串上使用 str.join()
:
1 | letters = ['s', 'p', 'a', 'm'] |
这会将 word 变量赋值为 spam
。这个习语可以用在列表和元组中。
在集合体(collection)中查找一个项
有时我们需要在集合体中查找。让我们看看这两个选择,列表和集合(set),用如下代码举个例子:
1 | s = set(['s', 'p', 'a', 'm']) |
即使两个函数看起来完全一样,但因为 查找集合 是利用了 Python 中的『集合是可哈希』的特性,两者的查询性能是非常不同的。为了判断一个项是否在列表中,Python 将会查看每个项直到它找到匹配的项。这是耗时的任务,尤其是对长列表而言。另一方面,在集合中, 项的哈希值将会告诉 Python 在集合的哪里去查找匹配的项。结果是,即使集合很大,查询的速度也很快。在字典中查询也是同样的原理。想了解更多内容,请见 StackOverflow 。想了解在每种数据结构上的多种常见操作的花费时间的详细内容, 请见 此页面。
因为这些性能上的差异,在下列场景中,使用集合或者字典而不是列表,通常会是个好主意:
- 集合体中包含大量的项;
- 你将在集合体中重复地查找项;
- 你没有重复的项。
对于小的集合体、或者你不会频繁查找的集合体,建立哈希带来的额外时间和内存的开销经常会大过改进搜索速度所节省的时间。
Python之禅
又名 PEP 20, 是 Python 设计的指导原则。
1 | >>> import this |
想要了解一些 Python 优雅风格的例子,请见 这些来自于 Python 用户的幻灯片。
PEP 8
PEP 8 是 Python 实际意义上的代码风格指南,我们可以在 pep8.org 上获得高质量的、可读性更高的 PEP 8 版本。
强烈推荐阅读这部分。整个 Python 社区都尽力遵循本文档中规定的准则。这其中,一些项目可能受其影响, 而其他项目可能 修改其建议。
也就是说,让你的 Python 代码遵循 PEP 8 通常是个好主意,这也有助于在与其他开发人员 一起工作时使代码更加具一致性。命令行程序 pycodestyle https://github.com/PyCQA/pycodestyle (以前叫做pep8
),可以检查代码一致性。在你的终端上运行以下命令来安装它:
1 | $ pip install pycodestyle |
然后,对一个文件或者一系列的文件运行它,来获得任何违规行为的报告:
1 | $ pycodestyle optparse.py |
程序 autopep8 能自动将代码格式化成 PEP 8 风格。用以下指令安装此程序:
1 | $ pip install autopep8 |
用以下指令格式化一个文件:
1 | $ autopep8 --in-place optparse.py |
不包含 --in-place
标志将会使得程序直接将更改的代码输出到控制台,以供审查。 --aggressive
标志则会执行更多实质性的变化,而且可以多次使用以达到更佳的效果。
约定
这里有一些你应该遵循的约定,以让你的代码更加易读。
检查变量是否等于常量
你不需要明确地比较一个值是 True,或者 None,或者 0 - 你可以仅仅把它放在 if
语句中。 参阅 真值测试 来了解什么被认为是 false:
糟糕:
1 | if attr == True: |
优雅:
1 | # 检查值 |
访问字典元素
不要使用 dict.has_key()
方法。 相反,使用 x in d
语法,或者将默认参数传递给 dict.get()
方法。
坏的示例:
1 | d = {'hello': 'world'} |
推荐的示例:
1 | d = {'hello': 'world'} |
操作列表的简便方法
列表推导式 提供了一个强大并且简洁的方法来对列表价进行操作。除此之外,map()
和 filter()
函数在列表的操作上也是非常简洁的。
坏:
1 | # Filter elements greater than 4 |
好:
1 | a = [3, 4, 5] |
坏:
1 | # Add three to all list members. |
好:
1 | a = [3, 4, 5] |
使用 enumerate()
来跟踪正在被处理的元素索引。
1 | a = [3, 4, 5] |
比起手动计数,使用enumerate()
函数有更好的可读性,而且,他更加适合在迭代器中使用。
读文件
使用 with open
语法来读文件,它能够为你自动关闭文件。
坏:
1 | f = open('file.txt') |
好:
1 | with open('file.txt') as f: |
即使在 with
控制块中出现了异常,它也能确保你关闭了文件,因此,使用 with
语法是更加优雅的。
行的延续
当一个代码逻辑行的长度超过可接受的限度时,你需要将之分为多个物理行。如果行的结尾是一个反斜杠,Python 解释器会把这些连续行拼接在一起。这在某些情况下很有帮助, 但我们总是应该避免使用,因为它的脆弱性:如果在行的结尾,在反斜杠后加了空格,这会破坏代码,而且可能有意想不到的结果。
一个更好的解决方案是在元素周围使用括号。左边以一个未闭合的括号开头,Python 解释器会把行的结尾和下一行连接起来直到遇到闭合的括号。同样的行为适用中括号和大括号。
糟糕:
1 | my_very_big_string = """For a long time I used to go to bed early. Sometimes,\ |
优雅:
1 | my_very_big_string = ( |
尽管如此,通常情况下,必须去分割一个长逻辑行意味着你同时想做太多的事,这可能影响可读性。