代码风格

../_images/33907150054_5ee79e8940_k_d.jpg

如果您询问Python程序员他们最喜欢什么样的Python,他们通常会引用它的高可读性。事实上,高度的可读性是Python语言设计的核心,它遵循了一个公认的事实,即代码读取的频率比编写的频率要高得多。

Python代码可读性高的一个原因是它相对完整的一套代码风格准则和“pythonic”习语。

当一个资深的python开发人员(pythonista)调用代码的一部分而不是“pythonic”时,通常意味着这些代码行不遵循通用的指导原则,并且无法以最佳(hear:most readable)方式表达其意图。

在一些边界案例中,关于如何在Python代码中表达意图的最佳方法尚未达成一致,但这些案例很少。

一般概念

显式代码

虽然任何一种黑魔法都可以通过Python实现,但最好是最明确和直接的方式。

Bad

def make_complex(*args):
    x, y = args
    return dict(**locals())

Good

def make_complex(x, y):
    return {'x': x, 'y': y}

在上面的好代码中,x和y是从调用方显式接收的,并返回显式字典。使用此函数的开发人员通过读取第一行和最后一行确切地知道要做什么,而坏例子并非如此。

每行一个语句

虽然有些复合语句(如列表理解)因其简洁性和表达性而被允许和欣赏,但在同一行代码上有两个不连贯的语句是不好的做法。

Bad

print 'one'; print 'two'

if x == 1: print 'one'

if <complex comparison> and <other complex comparison>:
    # do something

Good

print 'one'
print 'two'

if x == 1:
    print 'one'

cond1 = <complex comparison>
cond2 = <other complex comparison>
if cond1 and cond2:
    # do something

函数参数

参数可以通过四种不同的方式传递给函数。

  1. 位置参数 是必需的,没有默认值。它们是参数的最简单形式,可以用于几个函数参数,这些参数完全是函数意义的一部分,它们的顺序是自然的。例如,在 send(message, recipient)point(x, y) 函数的用户不难记住,这两个函数需要两个参数,并且按照哪个顺序。

在这两种情况下,调用函数时可以使用参数名,这样就可以切换参数的顺序,例如调用 send(recipient='World', message='Hello')point(y=2, x=1) 但与更直接的 send('Hello', 'World')point(1, 2) .

  1. 关键字参数 不是必需的,并且具有默认值。它们通常用于发送给函数的可选参数。当一个函数有两个或三个以上的位置参数时,它的签名更难记住,并且使用带有默认值的关键字参数很有帮助。例如,一个更完整的 send 函数可以定义为 send(message, to, cc=None, bcc=None) . 在这里 ccbcc 是可选的,并评估为 None 当它们没有被传递给另一个值时。

在Python中,可以通过多种方式调用带有关键字参数的函数;例如,可以按照定义中参数的顺序进行调用,而无需显式命名参数,如 send('Hello', 'World', 'Cthulhu', 'God') 把一份空白的副本寄给上帝。也可以按其他顺序命名参数,如 send('Hello again', 'World', bcc='God', cc='Cthulhu') .最好避免这两种可能性,不要有任何强烈的理由不遵循最接近函数定义的语法: send('Hello', 'World', cc='Cthulhu', bcc='God') .

作为旁注,遵循 YAGNI 原则上,删除添加了“以防万一”且似乎从未使用过的可选参数(及其函数内部的逻辑)通常比在需要时添加新的可选参数及其逻辑更困难。

  1. 这个 任意参数列表 是向函数传递参数的第三种方法。如果函数意图更好地由具有可扩展数量的位置参数的签名表示,则可以使用 *args 构造。在功能体中, args 将是所有剩余位置参数的元组。例如, send(message, *args) 可以用每个收件人作为参数调用: send('Hello', 'God', 'Mom', 'Cthulhu') 以及在函数体中 args 将等于 ('God', 'Mom', 'Cthulhu') .

然而,这个构造有一些缺点,应该谨慎使用。如果一个函数接收到一个具有相同性质的参数列表,那么通常更清楚地定义它为一个参数的函数,该参数是一个列表或任何序列。这里,如果 send 有多个收件人,最好明确定义: send(message, recipients) 然后打电话给 send('Hello', ['God', 'Mom', 'Cthulhu']) .这样,函数的用户可以预先将收件人列表作为一个列表来操作,并且可以传递任何不能作为其他序列解包的序列,包括迭代器。

  1. 这个 任意关键字参数字典 是向函数传递参数的最后一种方法。如果函数需要一系列未确定的命名参数,则可以使用 **kwargs 构造。在功能体中, kwargs 将是所有传递的命名参数的字典,这些参数未被函数签名中的其他关键字参数捕获。

注意事项与 任意参数列表 是必要的,出于类似的原因:当有必要使用这些强大的技术时,可以使用这些技术;如果更简单、更清晰的结构足以表达功能的意图,则不应使用这些技术。

由程序员编写函数来决定哪些参数是位置参数,哪些是可选关键字参数,以及决定是否使用高级的任意参数传递技术。如果明智地遵循上面的建议,那么编写以下python函数是可能的,也是令人愉快的:

  • 易于阅读(名称和参数无需解释)

  • 易于更改(添加新的关键字参数不会破坏代码的其他部分)

避开魔杖

对于黑客来说,python是一个强大的工具,它有一套非常丰富的钩子和工具,可以让你做几乎任何一种棘手的技巧。例如,可以执行以下每项操作:

  • 更改对象的创建和实例化方式

  • 更改python解释器导入模块的方式

  • 甚至可以(如果需要,建议)在Python中嵌入C例程。

然而,所有这些选项都有许多缺点,最好使用最直接的方式来实现您的目标。主要的缺点是在使用这些结构时,可读性会受到很大的影响。许多代码分析工具,如pylint或pyflakes,将无法解析这个“魔力”代码。

我们认为,Python开发人员应该了解这些几乎无限的可能性,因为它会给人们灌输信心,即不会有任何无法解决的问题。然而,知道如何,尤其是何时 not 使用它们是非常重要的。

像功夫大师一样, Python 知道如何用一根手指杀人,而且从来不会真的这么做。

我们都是负责任的用户

如上所述,python允许使用许多技巧,其中一些技巧可能很危险。一个很好的例子是,任何客户机代码都可以覆盖对象的属性和方法:Python中没有“private”关键字。这种哲学,与Java这样的高度防御语言非常不同,它提供了很多防止任何误用的机制,用“我们都是负责任的用户”这一说法来表达。

这并不意味着,例如,没有属性被认为是私有的,在Python中也不可能进行适当的封装。相反,Python社区更倾向于依赖一组约定,指示不应该直接访问这些元素,而不是依赖开发人员在代码和其他代码之间建立的混凝土墙。

私有属性和实现细节的主要约定是在所有“内部”前面加下划线。如果客户机代码违反了此规则并访问了这些标记的元素,那么在修改代码时遇到的任何错误行为或问题都是客户机代码的责任。

鼓励慷慨地使用此约定:任何不打算由客户机代码使用的方法或属性都应该以下划线作为前缀。这将保证更好的职责分离和更容易修改现有的代码;公开私有财产总是可能的,但是将公共财产私有化可能是一个更困难的操作。

返回值

当一个函数的复杂性增加时,在函数体中使用多个返回语句并不少见。然而,为了保持清晰的意图和可持续的可读性水平,最好避免从身体的许多输出点返回有意义的值。

函数中返回值的主要情况有两种:函数正常处理时返回的结果,以及指示错误输入参数或函数无法完成其计算或任务的任何其他原因的错误情况。

如果不希望引发第二种情况的异常,则返回一个值,例如none或false,表示可能需要函数无法正确执行。在这种情况下,最好在检测到不正确的上下文时尽早返回。它将有助于扁平化函数的结构:返回后的所有代码都可以假定满足条件以进一步计算函数的主要结果。通常需要有多个这样的返回语句。

但是,当一个函数的正常过程中有多个主出口点时,调试返回的结果变得困难,因此最好保留一个出口点。这也将有助于分解出一些代码路径,并且多个出口点可能表示需要这样的重构。

def complex_function(a, b, c):
    if not a:
        return None  # Raising an exception might be better
    if not b:
        return None  # Raising an exception might be better
    # Some complex code trying to compute x from a, b and c
    # Resist temptation to return x if succeeded
    if not x:
        # Some Plan-B computation of x
    return x  # One single exit point for the returned value x will help
              # when maintaining the code.

语法

简单地说,编程语法是 way 编写代码。编程习语的概念在 c2Stack Overflow .

惯用的python代码通常被称为 Python 的 .

尽管通常有一种——最好只有一种——显而易见的方法来做到这一点; the 编写惯用的python代码的方法对于python初学者来说是不明显的。所以,好的习语必须有意识地习得。

一些常见的python习惯用法如下:

拆包

如果知道列表或元组的长度,可以通过解包为其元素指定名称。例如,因为 enumerate() 将为列表中的每个项提供两个元素的元组:

for index, item in enumerate(some_list):
    # do something with index and item

您也可以使用它交换变量:

a, b = b, a

嵌套解包也可以工作:

a, (b, c) = 1, (2, 3)

在python 3中,引入了一种新的扩展解包方法 PEP 3132

a, *rest = [1, 2, 3]
# a = 1, rest = [2, 3]
a, *middle, c = [1, 2, 3, 4]
# a = 1, middle = [2, 3], c = 4

创建忽略的变量

如果您需要分配某些内容(例如, 拆包 )但不需要那个变量,使用 __

filename = 'foobar.txt'
basename, __, ext = filename.rpartition('.')

注解

许多python风格的指南建议对一次性变量使用一个下划线“`”,而不是这里推荐的双下划线“``”。问题是,“``````通常用作 gettext() 函数,并在交互提示下用于保存上一个操作的值。相反,使用双下划线也同样清晰和方便,并且消除了意外干扰这些其他用例的风险。

创建一个相同事物的length-n列表

使用python列表 * 操作员:

four_nones = [None] * 4

创建一个长度为n的列表列表

因为列表是可变的, * 操作员(如上所述)将创建一个对 same 列表,这不太可能是您想要的。相反,使用列表理解:

four_lists = [[] for __ in xrange(4)]

注意:在python 3中使用range()而不是xrange()。

从列表中创建字符串

创建字符串的常见习惯用法是 str.join() 在空字符串上。

letters = ['s', 'p', 'a', 'm']
word = ''.join(letters)

这将设置变量的值 word “垃圾邮件”。这个习语可以应用于列表和元组。

在集合中搜索项目

有时我们需要搜索一组东西。让我们来看两个选项:列表和集合。

以下面的代码为例:

s = set(['s', 'p', 'a', 'm'])
l = ['s', 'p', 'a', 'm']

def lookup_set(s):
    return 's' in s

def lookup_list(l):
    return 's' in l

即使两个函数看起来相同,因为 lookup_set 利用python中的集合是哈希表这一事实,两者之间的查找性能非常不同。要确定一个项目是否在列表中,python必须遍历每个项目,直到找到匹配的项目。这很费时,尤其是对于长的列表。另一方面,在一个集合中,项目的散列将告诉python在集合中的哪里查找匹配的项目。因此,即使集合很大,搜索也可以很快完成。在字典中搜索的工作原理是一样的。有关详细信息,请参阅 StackOverflow 第页。有关各种常见操作对这些数据结构执行的时间的详细信息,请参阅 this page .

由于这些性能差异,在以下情况下,最好使用集合或字典而不是列表:

  • 集合将包含大量项

  • 您将重复搜索集合中的项目

  • 您没有重复的项目。

对于较小的集合,或者您不经常搜索的集合,设置哈希表所需的额外时间和内存通常会大于通过提高搜索速度节省的时间。

Zen of Python

也称为 PEP 20 ,Python设计的指导原则。

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

有关良好的Python样式的一些示例,请参见 these slides from a Python user group .

PEP 8

PEP 8 实际上是Python的代码样式指南。PEP 8的高质量、易读版本也可在 pep8.org .

强烈建议阅读。整个Python社区都尽最大努力遵守本文档中列出的指导原则。有些项目可能会不时地受到影响,而另一些项目可能会 amend its recommendations .

也就是说,让您的Python代码符合PEP8通常是一个好主意,有助于使代码在与其他开发人员一起处理项目时更加一致。有一个命令行程序, pycodestyle (以前称为 pep8 ,这可以检查代码的一致性。通过在终端中运行以下命令进行安装:

$ pip install pycodestyle

然后在一个文件或一系列文件上运行它,以获取任何违规的报告。

$ pycodestyle optparse.py
optparse.py:69:11: E401 multiple imports on one line
optparse.py:77:1: E302 expected 2 blank lines, found 1
optparse.py:88:5: E301 expected 1 blank line, found 0
optparse.py:222:34: W602 deprecated form of raising exception
optparse.py:347:31: E211 whitespace before '('
optparse.py:357:17: E201 whitespace after '{'
optparse.py:472:29: E221 multiple spaces before operator
optparse.py:544:21: W601 .has_key() is deprecated, use 'in'

程序 autopep8 可用于自动重新格式化PEP 8样式的代码。安装程序时使用:

$ pip install autopep8

使用它来设置文件的格式:

$ autopep8 --in-place optparse.py

不包括 --in-place 标志将使程序直接将修改后的代码输出到控制台以供查看。这个 --aggressive 标志将执行更多实质性的更改,并且可以多次应用以获得更大的效果。

Conventions

下面是一些您应该遵循的惯例,以使您的代码更容易阅读。

检查变量是否等于常量

您不需要显式地将值与true、none或0进行比较——只需将其添加到if语句中即可。参见 Truth Value Testing 一个被认为是错误的列表。

Bad

if attr == True:
    print 'True!'

if attr == None:
    print 'attr is None!'

Good

# Just check the value
if attr:
    print 'attr is truthy!'

# or check for the opposite
if not attr:
    print 'attr is falsey!'

# or, since None is considered false, explicitly check for it
if attr is None:
    print 'attr is None!'

访问字典元素

不要使用 dict.has_key() 方法。相反,使用 x in d 语法,或将默认参数传递给 dict.get() .

Bad

d = {'hello': 'world'}
if d.has_key('hello'):
    print d['hello']    # prints 'world'
else:
    print 'default_value'

Good

d = {'hello': 'world'}

print d.get('hello', 'default_value') # prints 'world'
print d.get('thingy', 'default_value') # prints 'default_value'

# Or:
if 'hello' in d:
    print d['hello']

操作列表的快捷方式

List comprehensions 提供一种使用列表的强大、简洁的方法。

Generator expressions 遵循几乎与列表理解相同的语法,但返回生成器而不是列表。

创建新列表需要更多的工作和内存。如果您只想循环访问新列表,那么最好使用迭代器。

Bad

# needlessly allocates a list of all (gpa, name) entires in memory
valedictorian = max([(student.gpa, student.name) for student in graduates])

Good

valedictorian = max((student.gpa, student.name) for student in graduates)

当您确实需要创建第二个列表时,例如,如果您需要多次使用结果,请使用列表理解。

如果您的逻辑对于简短的列表理解或生成器表达式来说过于复杂,请考虑使用生成器函数而不是返回列表。

Good

def make_batches(items, batch_size):
    """
    >>> list(make_batches([1, 2, 3, 4, 5], batch_size=3))
    [[1, 2, 3], [4, 5]]
    """
    current_batch = []
    for item in items:
        current_batch.append(item)
        if len(current_batch) == batch_size:
            yield current_batch
            current_batch = []
    yield current_batch

不要仅仅为了副作用而使用清单理解。

Bad

[print(x) for x in sequence]

Good

for x in sequence:
    print(x)

筛选列表

Bad

在迭代列表时,不要从列表中删除项目。

# Filter elements greater than 4
a = [3, 4, 5]
for i in a:
    if i > 4:
        a.remove(i)

不要在列表中进行多次传递。

while i in a:
    a.remove(i)

Good

使用列表理解或生成器表达式。

# comprehensions create a new list object
filtered_values = [value for value in sequence if value != x]

# generators don't create another list
filtered_values = (value for value in sequence if value != x)

修改原始列表可能产生的副作用

如果有其他变量引用原始列表,则修改该列表可能会有风险。但是你可以用 切片分配 如果你真的想这么做。

# replace the contents of the original list
sequence[::] = [value for value in sequence if value != x]

修改列表中的值

Bad

记住,赋值永远不会创建新对象。如果两个或多个变量引用同一个列表,则更改其中一个变量将更改所有变量。

# Add three to all list members.
a = [3, 4, 5]
b = a                     # a and b refer to the same list object

for i in range(len(a)):
    a[i] += 3             # b[i] also changes

Good

创建一个新的列表对象并让原始对象单独存在更安全。

a = [3, 4, 5]
b = a

# assign the variable "a" to a new list without changing "b"
a = [i + 3 for i in a]

使用 enumerate() 数一数你在名单上的位置。

a = [3, 4, 5]
for i, item in enumerate(a):
    print i, item
# prints
# 0 3
# 1 4
# 2 5

这个 enumerate() 函数具有比手动处理计数器更好的可读性。此外,它对迭代器进行了更好的优化。

从文件读取

使用 with open 从文件中读取的语法。这将自动为您关闭文件。

Bad

f = open('file.txt')
a = f.read()
print a
f.close()

Good

with open('file.txt') as f:
    for line in f:
        print line

这个 with 语句更好,因为它将确保始终关闭文件,即使在 with 块。

线路延续

当逻辑代码行长于接受的限制时,需要将其拆分为多个物理行。如果行的最后一个字符是反斜杠,python解释器将连接连续的行。这在某些情况下是有帮助的,但通常应该避免,因为它的脆弱性:在行的末尾加上一个空格,在反斜杠之后,将会破坏代码,并可能产生意想不到的结果。

更好的解决方案是在元素周围使用括号。在行的末尾留下一个未闭合的括号,python解释器将连接下一行,直到括号闭合。同样的行为适用于花括号和方括号。

Bad

my_very_big_string = """For a long time I used to go to bed early. Sometimes, \
    when I had put out my candle, my eyes would close so quickly that I had not even \
    time to say “I’m going to sleep.”"""

from some.deep.module.inside.a.module import a_nice_function, another_nice_function, \
    yet_another_nice_function

Good

my_very_big_string = (
    "For a long time I used to go to bed early. Sometimes, "
    "when I had put out my candle, my eyes would close so quickly "
    "that I had not even time to say “I’m going to sleep.”"
)

from some.deep.module.inside.a.module import (
    a_nice_function, another_nice_function, yet_another_nice_function)

但是,通常情况下,必须拆分一个长逻辑行是一个信号,表明您试图同时执行过多操作,这可能会妨碍可读性。