clean code

i create stuff

[翻译]Python漫游指南 - 编程风格

| Comments

原文: Code Style

如果你问Python程序员他们最喜欢Python的什么,他们通常会说是Python的可读性。实际上,良好的可读性是Python设计时的一个核心思想,这是根据一个广为认可的事实 比起写,代码更容易被人读 的思想设计的。

Python代码易读易懂的一个原因是它相对完善的代码风格的规范和Pythonic的成语。

另外,当一个Python老手(Pythonista)指出一段代码不够Pythonic,通常意味着这几行代码没有按照一般的代码规范来并且没有用公认的最好的(通常是最易读的)方式表达其意图。

在某些情况下是找不到一个大家都认可的方式去用Python表达一个意图的,不过很少有这种情况。

一般概念

明确的代码

虽然在Python中,一切黑魔法都有可能发生,但我们更推荐最明确和最直接的方式。
Bad

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

Good

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

在上面的正面例子中,x、y明确地被函数接收,返回了一个明确的字典。通过读第一行和最后一行,使用这个函数的开发者就能准确知道怎样使用这个函数了,然而反面教材就完全不是那回事了。

一行一个语句

虽然有些复合语句,比如列表推导式,因为它们的简洁和丰富的表达能力被允许和推荐使用,但是将两个不能联合使用的语句放到同一行是一种很糟糕的做法。
Bad

1
2
3
4
5
6
print 'one'; print 'two'

if x == 1: print 'one'

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

Good

1
2
3
4
5
6
7
8
9
10
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)这是没有必要的冗余。
2. 关键字参数
不是必须的并且有默认值。它们常常被作为可选参数传递给函数。当一个函数有超过2至3个可选参数时,它的特点就变得更让人难以记住了,这时使用有默认值的关键字参数就会很有用。比如,一个更完整的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 原则来,通常去掉一个“只是为了以防万一”而加上的可选参数(和函数当中的逻辑)比在需要时添加一个新的可选参数和它的逻辑要难。
3. 可变参数列表
第三种向函数传递参数的方法。如果一个函数的作用用一组数目可扩展的位置参数能更好地表示的话,它就能用*args结构定义。在函数体中,args会成为一个包含所有位置参数的元组。比如send(message, *args)可以将每个接收者作为参数被调用:send('Hello', 'God', 'Mom', 'Cthulhu')在函数体中args会等价于('God', 'Mom', 'Cthulhu')

但是这种结构有些缺陷,要小心使用。如果一个函数接受一系列具有共同特征的参数,通常将函数定义为接受一个列表或者其它序列的参数的函数会更清晰。在这里,如果send有多个接收者,最好显式地定义它:send(message, recipients)并用send('Hello', ['God', 'Mom', 'Cthulhu'])来调用它。这样,函数的使用者事先就能将接收者列表制造为一个列表,并且可以向其中传递任何的序列,包括不能被解包成其它序列的迭代器。
4. 可变关键字参数列表
最后一种向函数传递参数的方法。如果函数需要一组不确定的命名参数,就可以用kwargs结构。在函数体中,kwargs会是一个字典,其中包含所有没被函数特征中的其它关键字参数获取的有变量名的参数。

同样需要注意可变参数列表中的情形,因为类似的原因:这些强大的工具应该在的确有必要时才被用到,并且如果更简单、整洁的结构就足够表明函数的意图时就不应该使用它们。

哪些参数是位置参数、哪些参数是可选关键字参数和是否使用高级的可变参数是写函数的程序员决定的,如果能明智地遵循上面的建议,就可以写出这样的Python函数:

  • 易读(参数名不需要解释)
  • 易修改(添加一个新的关键字参数不会破坏代码的其它部分)

别用魔棒

作为黑客的强大工具,Python有非常多的钩子和工具,能让你做几乎任何的技巧性的玩法。比如,可以做这些事:

  • 修改对象是怎么创建和实例化的
  • 修改Python解释器是怎么导入模块的
  • 甚至可以(如果需要也推荐)在Python中嵌入C例程

但是所有这些选项都有很多缺点,用最直接的方法来达到你的目标总是会更好。最主要的缺点是用这些结构时可读性会大大降低。许多代码分析工具,像pylint或pyflakes,都不能解析这些“魔术”代码。

我们认为一个Python开发者应该了解这些几乎无穷无尽的可能性,因为这让我们具有信心,没有什么能阻碍我们。然而,知道怎么做、尤其是什么时候用它们非常重要。 r

就像一个功夫高手,一个Pythonista知道如何用一只手指杀人,实际上却从不这么干。

我们都是负责的用户

正如上面我们看到的,Python允许很多技巧,其中有些包含潜在的危险。一个很好的例子就是,任何客户端代码都可以重载一个对象的属性和方法:Python中没有private关键字。这种和像Java这样防御性很强、有很多手段来避免误用的语言非常不同的设计思想被称为“我们都是负责的用户”。

这不意味着没有属性被认为是私有的、Python中不可能进行合适的封装。相反,Python社区不依赖开发者在自己和别人的代码之间建一堵堵墙,而是更倾向于依赖一系列的公约来指示这些元素不应该被直接访问。

对私有变量和实现细节最主要的公约就是给所有“内部变量”加上下划线前缀。如果客户端代码破坏了这种规则访问了这些被标记的元素,任何因为代码修改导致的误操作或者问题都应该由客户端承担责任。

慷慨地使用这条公约是被鼓励的:任何不想被客户端代码使用的方法或属性都应该加上一个下划线前缀。这会保证责任更好地分离、现有代码更好修改;将私有属性公开化总是可以的,但是将公有属性私有化可能就要麻烦多了。

返回值

当一个函数变得越来越复杂,不难碰到在一个函数体中用多个return语句的情况。然而为了保证清晰的缩进和可持续发展的可读性水准,最好避免从很多函数体输出点返回有意义的值。

有两种主要情况会在函数体中返回值:函数的结果在函数正常执行后返回,和指示错误输入参数或者导致函数无法完成它的计算或任务的其它情况。

如果你不希望在第二种情况中抛出异常,可能就需要返回一个值,比如None或者False来指示错误的输入参数或者其它原因导致函数不能像需要的那样正确执行。在这种情况下,在错误的上下文被发现时,越早返回越好。这会让函数的结构变得扁平化:所有那条因为错误而返回的语句之后的代码都可以假设满足进一步计算函数主要结果的条件。通常是要有多个这样的返回语句的。

然而,当一个函数在正常流程中有多个主要退出点时,调试返回结果就变得困难了,所以保持一个退出点会更好。这对制造出代码轨迹也有好处,有多个退出点可能表明你的代码需要重构了。

1
2
3
4
5
6
7
8
9
10
def complex_function(a, b, c):
    if not a:
        return None  # 抛出异常会更好
    if not b:
        return None  # 抛出异常会更好
    # 一些用a、b和c来计算x的复杂代码
    # 如果成功了要忍住返回x的诱惑
    if not x:
        # 一些对x的Plan-B的计算
    return x  # 维护代码时,一个为返回值x设置的单退出点会很好

成语

一个编程成语简单来说就是写代码的一种方法。编程成语的想法在c2Stack Overflow中已经得到了充分说明。

地道的Python代码通常被称作很Pythonic。

尽管通常有一种,并且更可取地仅仅只有一种明显的方法来解决问题;那种写地道的Python代码的方法,对于新人来说可能很不明显。所以好的成语需要有意识地去掌握。

下面介绍几个常用的成语:

解包

如果你知道一个列表或元组的长度,你可以通过解包给其中的元素命名。比如enumerate()会为列表中的每一项提供一个二元组:

1
2
for index, item in enumerate(some_list):
    # 用index和item来干点什么

你也可以用它来交换两个变量:

1
a, b = b, a

嵌套序列解包也行:

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

Python3中,PEP 3132介绍了一个扩展的解包的新方式:

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

创建一个会被忽略的变量

如果你需要给什么东西赋值(比如在序列解包中),但是后面不会用到那个变量,用__

1
2
filename = 'foobar.txt'
basename, __, ext = filename.rpartition('.')
注意:
很多Python风格指南推荐对需要抛弃的变量使用单下划线"_",而不是这里推荐的双下划线"__"。问题是单下划线"_"通常是作为[gettext()](http://docs.python.org/library/gettext.html#gettext.gettext)函数的别名来使用的,并且还被在交互式提示环境中来保存上一次操作用。用双下划线来替换掉它可以和它一样清晰,也几乎差不多方便,并且还消除了意外干扰其它用到它的情况的风险。

创建一个长度为N的相同元素的列表

使用列表*操作符

1
four_nones = [None] * 4

创建一个长度为N的元素为列表的列表

因为列表是可变的,*操作符(像上面的一样)会创造一个包含N个对相同列表的引用的列表,这可能不是你想要的。我们用列表推导来代替它:

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

一个创建字符串的常见成语是对空字符串调用str.join()

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

这会将变量word赋值为’spam’。这条成语可以用在列表和元组上。

有时候我们需要在在一个集合中进行查找。让我们来看看两种选择:列表和字典。
例如下面的代码:

1
2
3
4
5
6
7
8
d = {'s': [], 'p': [], 'a': [], 'm': []}
l = ['s', 'p', 'a', 'm']

def lookup_dict(d):
    return 's' in d

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

尽管这两个函数看起来几乎一模一样,因为lookup_dict利用了Python中的字典是哈希表的事实,这两个查找函数之间的性能差异是很大的。Python会不得不遍历列表中的每个项目来找到匹配的情形,这是非常耗时的。通过分析字典的哈希,在字典中查找键值可以被非常迅速地完成。要获得更多信息看这个StackOverflow页面

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风格的例子,看这个Stack Overflow的问题这些来自一个Python用户组的幻灯

PEP 8

PEP 8是Python实际上执行的编程风格指南。

将你的Python代码装换成PEP 8的整体上是个很好的想法,这能让你的代码在和其他开发者一起做项目时更持续。有一个命令行程序,pep8可以检查你的代码是否符合要求。在终端中输入下面的命令来安装它:

$ pip install pep8

在你的一个或者一系列文件跑一下来查看是否有任何冲突:

$ pep8 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'

约定

这里有些约定你应该遵守来让你的代码更易读。

检查一个变量是否等于常量

你不用显式地将一个值与True或None或0进行比较,你可以将它加到if语句后就行了。查看Truth Value Testing来查看一个值为false的列表。

Bad

1
2
3
4
5
if attr == True:
    print 'True!'

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

Good

1
2
3
4
5
6
7
8
9
10
11
# 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()方法,用key in d语法来代替它,或者向dict.get()方法传递一个默认参数。
Bad

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

Good

1
2
3
4
5
6
7
8
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']

更短的操纵列表的方法

列表推导提供了一种强大的、简洁的方法来操纵列表。同样地,map()filter()函数可以用一种不同的、更简洁的语法对列表执行操作。
Bad

1
2
3
4
5
6
# Filter elements greater than 4
a = [3, 4, 5]
b = []
for i in a:
    if i > 4:
        b.append(i)

Good

1
2
3
4
a = [3, 4, 5]
b = [i for i in a if i > 4]
# Or:
b = filter(lambda x: x > 4, a)

Bad

1
2
3
4
# Add three to all list members.
a = [3, 4, 5]
for i in range(len(a)):
    a[i] += 3

Good

1
2
3
4
a = [3, 4, 5]
a = [i + 3 for i in a]
# Or:
a = map(lambda i: i + 3, a)

使用enumerate()来对列表中你的位置进行计数。

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

enumerate()函数比手动处理计数器的可读性更好。更重要的是,它对迭代器的优化更好。

读文件

with open语法来读文件,这样会自动地为你关闭文件。

Bad

1
2
3
4
f = open('file.txt')
a = f.read()
print a
f.close()

Good

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

with语句会更好因为它会保证你总是会关闭文件,甚至在with块中有异常抛出的情况下。

换行

当代码逻辑上的一行比可接受的限制长时,你需要将它分割到多个物理行中。如果行尾是一个反斜杠,Python解释器会将连续的行连接起来。在有些情况下这很有用,但是通常应该避免这样做,因为这很脆弱:将一个空格加到行尾的反斜杠后就会破坏代码并且可能造成意料之外的错误。

一个更好的解决方案是在你的元素周围使用括号。Python解释器会将一个行尾未闭合括号后的下一行连接起来,直到括号闭合。同样的行为对花括号和方括号也成立。

Bad

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
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)

然而更多情况下,不得不分割一个逻辑行意味着你试图同时做太多事,这可能会降低可读性。

译后记

这篇文章的翻译动笔是在3月22日,今天已经是4月16日了,中间拖延了将近一个月,实际上大部分都是我今天一口气翻译完的。而且由于这篇文章一直没翻译完,导致想写的其它几篇也不能开始写。首先自我检讨下,确实没把这篇文章的翻译放在心上,因为找实习一直在打码,不过如今实习找完了,就有时间充实下自己和自己的博客了。另外,本来对这篇文章期望值相当高的,结果翻译完发现大部分内容我早已掌握,对我的帮助实际上不大,这种一直在语法上绕来绕去其实还是有点无聊,后面会翻译或者写一些更有实用价值的文章。

翻译这件事还是很难的,不过翻完这篇长文,我也掌握了一些技巧,比如it’s开头的无主被动句型要调整语序、灵活地选用更地道的汉语、长句子拆分为多个短句……总得来说还是熟能生巧。

另外,有译得不好的地方请在留言指出,请你吃棒棒糖。

Comments