原文:http://python.jobbole.com/84618/html
本文超出 PEP8 的范畴以涵盖我认为优秀的 Python 风格。本文虽然坚持己见,却不偏执。不只仅涉及语法、模块布局等问题,同时深刻范式、组织及架构的领域。但愿本文能成为精简版 Python 代码《风格的要素》。python
…… 可是,命名和单行长度更灵活。git
PEP8 涵盖了诸如空格、函数/类/方法之间的换行、import
、对已弃用功能的警告之类的寻常东西,大都不错。github
应用这些准则的最佳工具是 flake8,还能够用来发现一些愚蠢的语法错误。web
PEP8 本来只是一组指导原则,没必要严格甚至虔诚地信奉。必定记得阅读 PEP8 「愚蠢的一致性就是小人物的小妖精」一节。若要进一步了解,能够听一下 Raymond Hettinger 的精彩演讲,「超越 PEP8」。正则表达式
惟一引发过多争议的准则事关单行长度和命名。要调整起来也不难。数据库
如果厌烦 flake8
死板的单行长度不得超过 79 个字符的限制,彻底能够忽略或修改这一准则。这仍然不失为一条不错的经验法则,就像英语中句子不能超过 50 个单词,段落不能超过 10 个句子之类的规则同样。这是 flake8 配置文件 的连接,能够看到 max-line-length
配置选项。值得注意的是,能够给要忽略 flake8
检查的那一行加上 # noqa
注释,可是请勿滥用。编程
尽管如此,超过九成的代码行都不该该超过 79 个字符,缘由很简单,「扁平胜于嵌套」。若是函数每一行都超出了 79 个字符,确定有别的东西出错了,这时要看看代码而不是 flake8
配置。json
关于命名,遵循几条简单的准则就能够避免众多足以影响整个小组的麻烦。vim
下面这些准则大多改编自 Pacoo 小组。
驼峰式
和首字母缩略词:HTTPWriter
优于 HttpWriter
。lower_with_underscores
。lower_with_underscores
。lower_with_underscores.py
。(可是不带下划线的名字更好!)UPPER_WITH_UNDERSCORES
。name_re
。一般都应该遵循这些准则,除非要参照其余工具的命名规范,好比数据库 schema 或者消息格式。
还能够用 驼峰式
给相似类却不是类的东西命名。使用 驼峰式
的主要好处在于让人们以「全局名词」来关注某个东西,而不是看做局部标记或动词。值得注意的是,Python 给 True
,False
和 None
这些根本不是类的东西命名也是用 驼峰式
。
…… 好比 _prefix
或 suffix_
。函数和方法名能够用 _prefix
标记来暗示其是「私有的」,可是最好只在编写预期会普遍使用的 API 以及用 _prefix
标记来隐藏信息的时候谨慎使用。
PEP8 建议使用结尾的下划线来避免与内置关键字重名,好比:
1
2
|
sum_ = sum(some_long_list)
print(sum_)
|
临时这样用也能够,不过最好仍是选一个别的名字。
用 __mangled
这种双下划线前缀给类/实例/方法命名的状况很是少,这实际上涉及特殊的名字修饰,很是罕见。不要起 __dunder__
这种格式的名字,除非要实现 Python 标准协议,好比 __len__
;这是为 Python 内部协议保留的命名空间,不该该在其中增长自定义的东西。
(不过)一些常见的单字符名字能够接受。
在 lambda
表达式中,单参数函数能够命名为 x
。好比:
1
|
encode = lambda x: x.encode("utf-8", "ignore")
|
解包元组时能够用 _
丢弃不须要的标记。好比:
1
|
_, url, urlref = data
|
意思就是说「忽略第一个元素」。
和 lambda
相似,在解析列表/字典/集合的时候,以及在生成器表达式或者一到两行的 for 循环中,可使用单字符迭代标记。一般选择 x
,好比:
1
|
sum(x for x in items if x > 0)
|
能够求 items
序列中全部正整数之和。
此外比较常见的是 i
,表明 index
,一般和内置的 枚举
一块儿使用。好比:
1
2
|
for i, item in enumerate(items):
print("%4s: %s" % (i, item))
|
除却上述情形,要极少甚至避免使用单字符用做标记/参数/方法的名字。由于这样就没法用grep
进行检索了。
self
及相似的惯例应该:
self
@classmethod
的第一个参数命名为 cls
*args
和 **kwargs
不遵循以下准则没有什么好处,干脆照它说的作。
object
并使用新式类
1
2
3
4
5
6
7
|
# bad
class JSONWriter:
pass
# good
class JSONWriter(object):
pass
|
对于 Python 2 来讲遵循这条准则很重要。不过因为 Python 3 全部的类都隐式继承自 object
,这条准则就没有必要了。
1
2
3
4
5
6
7
8
9
10
|
# bad
class JSONWriter(object):
handler = None
def __init__(self, handler):
self.handler = handler
# good
class JSONWriter(object):
def __init__(self, handler):
self.handler = handler
|
1
2
3
4
5
|
# bad
map(truncate, filter(lambda x: len(x) > 30, items))
# good
[truncate(x) for x in items if len(x) > 30]
|
尽管在大多数简单状况下最好使用解析表达式,不过有时候 map()
或者 filter()
可读性更佳,须要本身判断。
(...)
折行
1
2
3
4
5
6
7
|
# bad
from itertools import groupby, chain,
izip, islice
# good
from itertools import (groupby, chain,
izip, islice)
|
(...)
写 API 更利落
1
2
3
4
5
6
7
8
9
|
# bad
response = Search(using=client)
.filter("term", cat="search")
.query("match", title="python")
# good
response = (Search(using=client)
.filter("term", cat="search")
.query("match", title="python"))
|
1
2
3
4
5
6
7
|
# bad -- simply unnecessary backslash
return set((key.lower(), val.lower())
for key, val in mapping.iteritems())
# good
return set((key.lower(), val.lower())
for key, val in mapping.iteritems())
|
isinstance(obj, cls)
, 不要用 type(obj) == cls
由于 isinstance
涵盖更多情形,包括子类和抽象基类。同时,不要过多使用 isinstance
,由于一般应该使用鸭子类型!
with
处理文件和锁with
语句可以巧妙地关闭文件并释放锁,哪怕是在触发异常的状况下。因此:
1
2
3
4
5
6
7
8
9
|
# bad
somefile = open("somefile.txt", "w")
somefile.write("sometext")
return
# good
with open("somefile.txt", "w") as somefile:
somefile.write("sometext")
return
|
None
相比较要用 is
None
值是一个单例,可是检查 None
的时候,实际上不多真的要在 左值上调用 __eq__
。因此:
1
2
3
4
5
6
7
|
# bad
if item == None:
continue
# good
if item is None:
continue
|
好的写法不只执行更快,并且更准确。使用 ==
并不会更简洁,因此请记住本规则!
sys.path
经过 sys.path.insert(0, "../")
等操做来控制 Python 的导入方法或许让人心动,可是要坚定避免这样作。
Python 有一套有几分复杂,却易于理解的模块路径解决方法。能够经过 PYTHONPATH
或诸如 setup.py develop
的技巧来调整 Python 导入模块的方法。还能够用 -m
运行 Python 获得须要的效果,好比使用 python -m mypkg.mymodule
而不是 python mypkg/mymodule.py
。代码可否正常运行不该依赖于当前执行 Python 的工做路径。David Beazley 用 PDF 幻灯片再一次扭转了你们的见解,值得一读,“Modules and Packages: Live and Let Die!”。
…… 若是必定要,也不要建立太多。
1
2
3
4
5
6
7
8
|
# bad
class ArgumentError(Exception):
pass
...
raise ArgumentError(url)
# good
raise ValueError("bad value for url: %s" % url)
|
要知道 Python 引入了 丰富的内建异常类。值得充分利用。并且经过描述那些触发特定错误条件的字符串消息,来实例化这些异常类,就能达到「定制」的目的。在用户代码中抛出 ValueError
(参数错误),LookupError
(键错误)以及 AssertionError
(用 assert
语句)最为常见。
至因而否应该本身建立异常类有一个不错的经验法则,也就是搞清楚函数调用方每次调用函数之时是否都应该捕获该异常。若是是,那么的确应该本身建立异常类。不过这至关少见。关于这类明显不得不使用的自定义异常类有一个不错的例子,tornado.web.HTTPError。可是要留心 Tornado 是如何避免走极端的:框架或用户代码抛出的全部 HTTP 错误同属一个异常类。
1
2
3
4
5
6
7
8
9
|
# bad
def reverse_sort(items):
"""
sort items in reverse order
"""
# good
def reverse_sort(items):
"""Sort items in reverse order."""
|
把三引号 """
放在同一行,首字母大写,以句号结尾。四行精简到两行,__doc__
属性没有糟糕的换行,最吹毛求疵的人也会满意的!
标准库和大多数开源项目皆是如此。Sphinx 提供支持,开箱即用。赶忙试试吧!Python requests
模块由此取得了极佳的效果。看看requests.api
模块的例子。
最挑剔也不过如此了吧,但是若作不到这一点,有些人可能会被逼疯。不乏能自动搞定这一切的编辑器;这是我用 vim 的实现。
下面是在函数文档字符串中使用 Sphinx 风格的 reST 的快速参考:
1
2
3
4
5
6
7
8
9
10
11
|
def get(url, qsargs=None, timeout=5.0):
"""Send an HTTP GET request.
:param url: URL for the new request.
:type url: str
:param qsargs: Converted to query string arguments.
:type qsargs: dict
:param timeout: In seconds.
:rtype: mymodule.Response
"""
return request('get', url, qsargs=qsargs, timeout=timeout)
|
不要为文档而写文档。写文档字符串要这样思考:
1
|
好名字
+ 显式指明默认值 优于 罗嗦的文档 + 类型的详细说明
|
也就是说,上例中没有必要说 timeout
是 float
,默认值 5.0
,显然是 float
。在文档中指出其语义是「秒」更有用,就是说 5.0
意思是 5 秒钟。同时调用方不知道 qsargs
应该是什么,因此用 type
注释给出提示,调用方也无从知道函数的预期返回值是什么,因此 rtype
注释是合适的。
最后一点。吉多·范罗苏姆曾说过,他对 Python 的主要领悟是「读代码比写代码频率更高」。直接结论就是有些文档有用,更多的文档有害。
基本上只须要给预计会频繁重用的函数写文档。若是给内部模块的每个函数都写上文档,最后只能获得更加难以维护的模块,由于重构代码之时文档也要重构。不要「船货崇拜」文档字符串,更不要用工具自动生成文档。
一般应该用函数而不是类。函数和模块是 Python 代码重用的基本单元,仍是最灵活的形式。类是一些 Python 功能的「升级路径」,好比实现容器,代理,描述符,类型系统等等。可是一般函数都是更好的选择。
或许有人喜欢为了更好地组织代码而将关联的函数归在类中。但这是错的。关联的函数应该归在模块中。
尽管有时能够把类看成「小型命名空间」(好比用 @staticmethod
)比较有用,一组方法更应该对同一个对象的内部操做有所贡献,而不只仅做为行为分组。
与其建立 TimeHelper
类,带有一堆不得不引入子类才能使用的方法,永远不如直接为时间相关的函数建立 lib.time
模块。类会增殖出更多的类,会增长复杂性,下降可读性。
生成器和迭代器是 Python 中最强大的特性 —— 应该掌握迭代器协议,yield
关键字和生成器表达式。
生成器不只仅对要在大型数据流上反复调用的函数十分重要,并且可让自定义迭代器更加简单,从而简化了代码。将代码重构为生成器一般能够在使得代码在更多场景下复用,从而简化代码。
Fluent Python 的做者 Lucinao Ramalho 经过 30 分钟的演讲,「迭代器和生成器: Python 之道」,给出了一个出色的,快节奏的概述。Python Essential Reference 和 Python Cookbook 的做者 David Beazley 有个深奥的三小时视频教程,「生成器:最后的前沿」,给出了使人知足的生成器用例的详细阐述。由于应用普遍,掌握这一主题很是有必要。
声明式编程优于命令式编程。代码应该代表你想要作什么,而不是描述如何去作。Python 的函数式编程概览介绍了一些不错的细节并给出了高效使用该风格的例子。
使用轻量级的数据结构更好,好比 列表
,字典
,元组
和集合
。将数据展开,编写代码对其进行转换,永远要优于重复调用转换函数/方法来构建数据。
一个例子是重构常见的列表解析:
1
2
3
4
5
6
|
# bad
filtered = []
for x in items:
if x.endswith(".py"):
filtered.append(x)
return filtered
|
应该改为 :
1
2
3
4
|
# good
return [x
for x in items
if x.endswith(".py")]
|
可是还有一个不错的例子是将 if
/elif
/else
链改为 字典
查询。
这是个从函数式编程社区借来的概念。这种函数和迭代器亦被描述为「无反作用」,「引用透明」或者有「不可变输入/输出」。
一个简单的例子,要避免这种代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# bad
def dedupe(items):
"""Remove dupes in-place, return items and # of dupes."""
seen = set()
dupe_positions = []
for i, item in enumerate(items):
if item in seen:
dupe_positions.append(i)
else:
seen.add(item)
num_dupes = len(dupe_positions)
for idx in reversed(dupe_positions):
items.pop(idx)
return items, num_dupes
|
这个函数应该从新写成:
1
2
3
4
5
6
|
# good
def dedupe(items):
"""Return deduped items and # of dupes."""
deduped = set(items)
num_dupes = len(items) - len(deduped)
return deduped, num_dupes
|
这是个惊人的例子。函数不只更加纯粹,并且更加精简了。不只更加精简,并且更好。这里的纯粹是说 assert dedupe(items) == dedupe(items)
在「好」版本中恒为真。在「坏」版本中, num_dupes
在第二次调用时恒为 0
,这会在使用时致使难以理解的错误。
这个例子也阐明了命令式风格和声明式风格的区别:改写后的函数读起来更像是对须要的东西的描述,而不是构建须要的东西的一系列操做。
函数应该尽量处理数据,而不是自定义的对象。简单的参数类型更好,好比字典
,集合
,元组
,列表
,int
,float
和 bool
。从这些扩展到标准库类型,好比 datetime
, timedelta
, array
, Decimal
以及 Future
。只有在真的必要时才使用自定义类型。
判断函数是否足够精简有个不错的经验法则,问本身参数和返回值是否老是能够 JSON 序列化。结果证实这个经验法则至关有用:能够 JSON 序列化一般是函数在并行计算时可用的先决条件。可是,就本文档而言,主要的好处在于:可读性,可测试性以及整体的函数简单性。
在「传统的面向对象编程语言」中,好比 Java 和 C++ ,代码重用是经过类的继承和多态或者语言声称的相似机制实现的。对 Python 而言,尽管可使用子类和基于类的多态,事实上在地道的 Python 程序中这些功能极少使用。
经过模块和函数实现代码重用更为广泛,经过鸭子类型实现动态调度更为常见。若是发现本身经过超类实现代码重用,停下来,从新思考。若是发现本身大量使用多态,考虑一下是否用 Python 的 dunder 协议或者鸭子类型策略会更好。
看一下另外一个不错的 Python 演讲,一位 Python 核心贡献者的 「不要再写类了」。演讲者建议,若是构建的类只有一个命名像一个类的方法(好比 Runnable.run()
),那么实际上只是用函数模拟了一个类,这时候应该停下来。由于在 Python 中,函数是「最高级的」类型,没有理由这样作。
可使用 Mixin 实现基于类的代码重用,同时不须要走极端使用类型层次。可是不要滥用。「扁平胜于嵌套」也适用于类型层次,因此应该避免仅仅为了分解行为而引入没必要要的必须层次的一层。
Mixin 实际上不是 Python 的特性,多亏了 Python 支持多重继承。能够建立基类将功能「注入」到子类中,而没必要构成类型层次的「重要」组成部分,只须要将基类列入 bases
列表中的第一个元素。
1
2
|
class APIHandler(AuthMixin, RequestHandler):
"""Handle HTTP/JSON requests with security."""
|
要考虑顺序,同时不妨记住:bases
自底向上构成层次结构。这里可读性的好处在于关于这个类所须要知道的一切都包含在类
定义自己:「它混入了权限行为,是专门定制的 Tornado RequestHandler。」
Python 有大量的 web,数据库等框架。Python 语言的一大乐趣在于建立自定义框架至关简单。使用开源框架时,应该注意不要将本身的「核心代码」和框架自己结合得过于紧密。
考虑为本身的代码建立框架的时候应当慎之又慎。标准库有不少内置的东西,PyPI 有的就更多了,并且一般你不会须要它。
Python 经过一些特性来支持 「元编程」,包括修饰器,上下文管理器,描述符,import 钩子,元类和抽象语法树(AST)转换。
应该可以自如地使用并理解这些特性,做为 Python 的核心组成部分这些特性有着充分地支持。可是应当意识到使用这些特性之时,也引入了复杂的失败场景。因此,要把为本身的代码建立元编程工具与决定「建立自定义框架」同等对待。它们意味着同一件事情。真要这么作的时候,把元编程工具写成独立的模块,写好文档!
许多人将 Python 的元编程工具和其对 「双下划线」或「dunder」方法(好比 __getattr__
)的支持混为一谈。
正如博文 ——「Python 双下划线,双倍惊喜」—— 所言,双下划线没有什么「特殊的」。它们只不过是 Python 核心开发人员为全部的 Python 内部协议所起的轻量级命名空间。毕竟,__init__
也是双下划线,没有什么神奇的。
的确,有些双下划线比其余的会致使更多使人困惑的结果,好比,没有很好的理由就重载操做符一般不是什么好主意。可是它们中也有许多,好比 __repr__
,__str__
,__len__
以及 __call__
是 Python 语言的完整组成部分,应该在地道的 Python 代码中充分利用。不要回避!
做为一位核心 Python 开发者,Barry Warsaw 曾经说过「Python 之禅」(PEP 20)被用做 Python 代码风格指南使他沮丧,由于这本是为 Python 的内部设计所做的一首小诗。也就是语言的设计以及语言的实现自己。不过必须认可,PEP 20 中有些行能够看成至关不错的地道 Python 代码指南,因此咱们就把它加上了。
这一条有些主观,实际上等同于问:接手代码的人会被折服仍是感到失望?若是接手的人就是三年后的你呢?
有时为了重构以去除重复的代码,会有一点抽象。应该可以将代码翻译成显现的英语而且大体了解它是干什么的。不该该有太多的「神奇之处」。
这一条很好理解。最好的函数没有嵌套,既不用循环也不用 if
语句。第二好的函数只有一层嵌套。若是有两层及以上的嵌套,最好重构成更小的函数。
一样,不要惧怕将嵌套的 if 语句重构为多部分布尔条件。好比:
1
2
3
4
|
# bad
if response:
if response.get("data"):
return len(response["data"])
|
最好写成:
1
2
3
|
# good
if response and response.get("data"):
return len(response["data"])
|
不要惧怕用 #
添加行注释。也不要滥用或者写过多文档。一点点逐行解释,一般颇有帮助。不要惧怕使用稍微长一些的名字,由于描述性更好。将 「response
」写成「rsp
」没有任何好处。使用 doctest 风格的例子在文档字符串中详细说明边界状况。简洁至上!
单独的except: pass
子句危害最大。永远不要使用。制止全部的异常实在危险。将异常处理限制在一行代码,而且老是将 except
处理器限制在特定的类型下。除此以外,能够自如地使用 logging
模块和 log.exception(…)
。
这虽是通用软件工程原则,可是特别适用于 Python 代码。大多数 Python 函数和对象均可以有易于解释的实现。若是难以解释,极可能是一个坏主意。一般能够经过「分而治之」将一个难以解释的函数重写成易于解释的函数,也就是分割成多个函数。
好吧,咱们篡改了「Python 之禅」中的这一行,原文中「命名空间」才是个绝妙的好主意。
不过说正经的,优雅却没有测试的代码简直比哪怕是最丑陋却测试过的代码还要差劲。至少丑陋的代码能够重构成优雅的,可是优雅的代码却不能重构为能够证实是正确的代码,至少不写测试是作不到的!因此,写测试吧!拜托!
咱们把宁愿不去解决的争论放在这个部分。不要由于这些重写别人的代码。这里的东西能够自由地交替使用。
str.format
仍是重载格式化操做符 %
str.format
更健壮,然而 %
使用 "%s %s"
printf 风格的字符串更加简洁。二者会永远共存。
若是须要保存 unicode,记得在格式模板中使用 unicode 字符串:
1
|
u"%s %s" % (dt.datetime.utcnow().isoformat(), line)
|
若是最后选择 %
,应该考虑 "%(name)s"
语法,从而可使用字典而不是元组,好比:
1
|
u"%(time)s %(line)s" % {"time": dt.datetime.utcnow().isoformat(), "line": line}
|
此外,不要从新发明轮子。str.format
有一点毫无疑问比 %
更好,那就是支持各类格式化模式,好比人性化的数字和百分数。直接用。
可是选择哪个都没有问题。咱们没有强制规定。
if item
仍是 if item is not None
本条和以前的对于 None
是用 ==
仍是 is
没有关系。这里咱们实际上利用了 Python 的 「真实性规则」来处理 if item
,这其实是「item 不是 None 或者空字符串」的简写。
Python 中的真实性有些复杂。显然第二种写法对于某些错误而言更安全。可是第一种写法在 Python 代码中很是常见,并且更短。对此咱们并无强制规定。
"""
Python 编译器在语法分析时,若是多个字符串之间没有东西,会自动将其拼接为一个字符串。好比:
1
2
3
4
|
msg = ("Hello, wayward traveler!n"
"What shall we do today?n"
"=>")
print(msg)
|
大体上等价于:
1
2
3
4
|
msg = """Hello, wayward traveler!
What shall we do today?
=>"""
print(msg)
|
第一种写法能够保持缩进整洁,可是须要丑陋的换行符,第二种写法不须要换行符,可是打破了缩进。咱们没有强制规定哪一种更好。
raise
事实上给 raise
语句传入异常类或者异常实例均可以。好比,下面两行代码大体等价:
1
2
|
raise ValueError
raise ValueError()
|
本质上,Python 会将第一种写法自动转换为第二种。或许第二个写法更好,即便没有其余缘由,它也能实际上提供一个有用的理由,就像有条有用的消息来解释为何 ValueError
会发生同样。可是这两种写法是等价的,不该该为这一点就重写代码。咱们不作强制规定。
咱们选择了一些「最佳组合」工具,以及像样的 Python 项目会用到的最小初始结构。
import datetime as dt
: 永远像这样导入 datetime
dt.datetime.utcnow()
: 优于 .now()
, 后者使用的是当地时间import json
: 数据交换的标准from collections import namedtuple
: 用来作轻量级数据类型from collections import defaultdict
: 用来计数/分组from collections import deque
: 快速的双向队列from itertools import groupby, chain
: 为了声明式风格from functools import wraps
: 用来编写合乎规范的装饰器argparse
: 为了构建「健壮的」命令行工具fileinput
: 用来快速构建易于和 UNIX 管道结合使用的工具log = logging.getLogger(__name__)
: 足够好用的日志from __future__ import absolute_import
: 修复导入别名python-dateutil
用来解析时间和日历pytz
用来处理时区tldextract
为了更好地处理 URLmsgpack-python
比 JSON 更加紧凑地编码futures
为了 Future/pool 并发原语docopt
用来快速编写一次性命令行工具py.test
用来作单元测试,与 mock
和 hypothesis
结合使用对全部的 Python 包和库而言:
__init__.py
:目录名用做包名!mypackage/__init__.py
优于 src/mypackage/__init__.py
mypackage/lib/__init__.py
优于 lib/__init__.py
mypackage/settings.py
优于 settings.py
README.rst
用来给新手描述本项目;使用 rstsetup.py
用来构建简单工具,好比 setup.py develop
requirements.txt
是为 pip
准备的包依赖环境dev-requirements.txt
是为 tests/local 准备的额外的依赖环境Makefile
用来简化 (!!!) build/lint/test/run 步骤另外,永远记得详细说明包依赖环境。
下面这些连接或许能够给你启发,有助于书写具备良好风格和品味的 Python 代码。
Counter
class 代码, 做者是 Raymond Hettingerrq.queue
模块, 原做者是 Vincent Driessen出发吧,写更具 Python 风格的代码!
1
2
|
$ python
>>> import antigravity
|