编写高质量代码-改善python程序的91个建议勘误

我对《编写高质量代码》的看法


  1. 整本书的示例代码风格比较差,一点也不是作者看重的pythonic。空格、大小写、命名规则、单字母名称等随处可见;
  2. 示例代码短缺、逻辑错误比较多。例如打印素数的代码明显错误,还有几处印刷导致代码缺行的;
  3. 大多数主题一带而过,比较适合开阔视野,具体应用需要额外的工作;

勘误


1. 第一章:建议1:(2)代码风格

“不应当过分地使用奇技淫巧”, 但c[::-1]并不等于list(reversed(c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = [1, 2, 3, 4]
c = 'abcdef'
print a[::-1]
print c[::-1]
print list(reversed(a))
print list(reversed(c))
==>
c[::-1]
>>> 'fedcba'
list(reversed(c))
>>> ['f', 'e', 'd', 'c', 'b', 'a']

2. 第一章:建议1:(3)标准库

当用%做格式化式,如果只有一个格式化参数,一般是直接格式化而不用单元素的元组。

1
2
3
4
print 'Hello %s!' % ('Tom', )
==>
print 'Hello %s!' % 'Tom'

3. 第一章:建议1:(3)标准库

format并不比%格式化更pythonic。format不能用于字符串中有{}原型的情况。另外根据pep8的风格说明,位置参数和值之间没有空格。

1
2
3
4
print '{greet} from {language}.'.format(greet = 'Hello World', language = 'Python')
==>
print '{greet} from {language}.'.format(greet='Hello,world', language='Python')

一般来说,由于format中没有提供格式化字符,需要调用参数的特殊方法,会比%慢。

1
2
3
4
5
%timeit '{greet} from {language}.'.format(greet = 'Hello World', language = 'Python')
1000000 loops, best of 3: 780 ns per loop
%timeit '%s from %s.' % ('Hello World', 'Python')
1000000 loops, best of 3: 202 ns per loop

4. 第一章:建议2:(1)要避免劣化代码:2)避免使用容易引起混淆的名称

a. 两个示例代码都不pythonic
b. 变量名一般用C语言式的varibale_name,而不是驼峰式的camelCase
c. 短作用域的临时变量用短小的名称反而比较好看;
d. 实例代码有问题,当没有找到num时应该返回False
e. 代码风格问题,操作符两边应该有空,参数之间逗号之前没有空格之后有空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 实例一
def funA(list, num):
for element in list:
if num==element:
return True
else: # pass这边根本没用, 没找到应该返回Fase
pass
# 实例二
def find_num(searchList, num):
for listValue in searchList:
if num==listValue:
return True
else:
pass
==>
def has_find(alist, num):
return num in alist

5. 第一章:建议4:4)

关于注释,编写可阅读的代码,推荐《编写可读代码的艺术》。
几个示例中的代码和注释都很糟糕。代码变量本身没有足够的意义,注释本身就是代码的重复。

6. 第一章:建议5:通过适当添加空行使代码布局更为优雅、合理

实例一的猜数代码写的并不好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import random
guesses_made = 0 # 变量初始化与使用的地方太远,移动到while之前比较好
name = raw_input('Hello! What is your name?\n')
number = random.randint(1, 20)
print 'Well, {0}, I am thinking of a number between 1 and 20.'.format(name)
while guesses_made < 6:
guess = int(raw_input('Take a guess: ')) # 没有处理异常,输入字符等情况
guesses_made += 1
if guess < number: # 分支是互斥的,用 if .. elif .. else 比较好
print 'Your guess is too low.'
if guess > number:
print 'Your guess is too high.'
if guess == number:
Break # 没有Break,是 break
if guess == number:
print 'Good job, ..'
else:
print 'Nope. ..'

7. 第二章:建议8:利用assert语句来发现问题

其中的assert语句的表达式是错误的。

1
2
3
4
assert expression [", " expression2]
==>
assert expression [, expression2]

__debug__是不能在程序中赋值,在python2.7中报SynaxError。如果python启动时指定-O-OO优化选项则__debug__为False,此时断言会被优化掉。

使用断言要区分错误异常异常需要用代码处理,如文件打开失败、网络连接失败、磁盘空间不足等这些属于异常情况(CornerCase); 错误是不应该发生的情况,预示程序存在bug。所以断言一般用于检查正常程序中不会出现,但是为了预防万一出现用于提示有bug的场合。

1
2
3
4
5
# 示例
while True:
print 'empty loop'
assert False, 'it can not happen.' # 不可能发生,但是万一到这就是bug。

8. 第二章:建议10:充分利用Lazy evaluation的特性:1)避免不必要..

这种in判断不太建议使用列表或者元组,而是使用set。在python中,set检查是O(1)而列表和元组是O(n)。

1
2
3
4
5
6
7
8
9
10
abbreviations = ['cf.', 'e.g.', 'ex.', 'etc.', 'fig', 'i.e.', 'Mr.', 'vs.']
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
if w in abbreviations:
pass
==>
abbreviations = set(['cf.', 'e.g.', 'ex.', 'etc.', 'fig', 'i.e.', 'Mr.', 'vs.'])
for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'):
if w in abbreviations:
pass

9. 第二章:建议12:不推荐使用type来进行类型检查

实例一的代码中type(n) is types.IntType。这个n的类型肯定不是types.IntType,而是UserInt(作者认为n的类型应该是int,“显然这种判断不合理这句”)。作者应该想说的是isinstance,但isinstance(n, int)返回的也是True

1
2
3
4
5
6
7
8
9
10
11
12
class UserInt(int):
pass
ui = UserInt(2)
type(ui)
>>> __main__.UserInt
isinstance(ui, UserInt)
>>> True
isinstance(ui, int)
>>> True

实例二中的isinstance("a", (str, unicode))这个可不pythonic。在python2中检查是否是字符串用basestring,python3中直接用str

1
2
3
4
if PY3:
isinstance("a", str)
else:
isinstance("a", basestring)

10. 第三章:建议19:(2)循环嵌套导入的问题

实例代码中c1和c2的循环嵌套与使用from .. import ..还是使用import没有关系。python的编译执行是按照代码块而不是逐行。当编译执行代码块中的代码引用其他模块,而其他模块又引用了当前代码块或者还未编译的代码块时就会导致循环引用。而且循环引用除去报ImportError,还可能报AttributeError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 当前代码块 -> 引用其他模块 -> 其他模块引用 当前代码块 或者还未编译代码 -> 循环引用 -> Error
# c1.py
from c2 import g
def x():
pass
# c2.py
from c1 import x
def g():
pass
python c1.py
>>> ImportError: cannot import name g
# c1.py -> from c2 import g -> c2.py -> from c1 import x -> x代码块还未编译 -> 引发ImportError

11. 第三章:建议13:使用else子句简化循环

a. 使用else是否能够pythonic不太清楚。else有时能够简化逻辑,但却与直觉相反会使代码不易读懂。(也许是与我自己的直觉相反,我总以为else是条件异常也就是break才执行;但在这里是额外的、附加的的意思,循环正常时执行);
b. 示例代码中的print_prime函数逻辑错误,打印给定n内的素数不包含1以及n本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def print_prime(n):
for i in range(2, n):
found = True
for j in range(2, i):
if i % j == 0:
found = False
break
if found:
print i,
print_prime(1)
>>>
print_prime(2)
>>>
print_prime(5)
>>>2, 3
==>
def is_prime(n):
for i in range(2, n):
if n % i == 0:
return False
return True
def print_prime(n):
for i in range(1, n+1):
if is_prime(i):
print i,
print_prime(1)
>>> 1
print_prime(2)
>>> 1, 2
print_prime(5)
>>> 1, 2, 3, 5

12. 第三章:建议24:遵循异常处理的几点基本原则

图3-1中的图中少了一些执行流。try异常没有被捕获而finally没有异常时,异常会被抛出。

1
2
3
4
5
6
7
try:
raise IOError('test')
finally:
print 'finally'
>>> finally
>>> IOError: 'test'

13. 第三章:建议26:深入理解None

因为None0[]()的布尔值都为False,所以在0表示数字而非空的含义时,应该用is None来区分。

1
2
3
4
5
6
7
8
def print_your_salary(n):
if n is None: # 显然None与0的含义是不一样的
raise ValueError('can not be None')
if not n:
print 'Aha, you have No salary?'
else:
print 'rich man', n

14. 第三章:建议30:[],()和{}:一致的容器初始化形式

“本节开头的例子改用列表解析,直接可将代码行数减少为2行,这在大型复杂应用程序中尤为重要,…”。列表解析比较简洁清晰,但是不要过度使用(像下面这样其实很难直接看懂)。简单的追求代码短小是没有价值的,简洁清晰明确的表达算法才最重要。

1
2
3
4
5
6
7
8
9
10
11
# 这个列表解析式并不被推荐,前后的if以及else让人很难第一时间看清意图
[v**v if v%2 == 0 else v+1 for v in [2, 3, 4, -1] if v>0]
==>
alist = []
for v in [2, 3, 4, -1]:
if v > 0:
if v % 2:
alist.append(v*v)
else:
alist.append(v+1)

字典解析式的语法是{key_exp: value_exp for iter if cond},中间需要冒号。

15. 第三章:建议32:警惕默认参数潜在的问题

最后一个示例代码,当然不能传入time.time(),但是也不能传入time.time这个函数本身。

1
2
3
4
5
6
def report(when=time.time):
pass
==>
def report(when=None):
when = when or time.time()

16. 第四章:建议36:掌握字符串的基本用法

“空白符由string.whitespace常量定义”这句话表述不太正确。应该是“空白符的定义与string.whitespace默认值一样”更加贴切。简单来说改变string.whitespace的值并不会改变字符串strip等操作的行为。

1
2
3
4
5
6
7
import string
string.whitesapce = ','
s = ' abcde '
s.strip()
s
>>> 'abcde' # 行为并没有改变

17. 第四章:建议37:按需选择sort()或者sorted()

a. sort是列表对象自带的方法(不是函数);sorted是内建的函数。
b. sort会改变列表对象本身;sorted返回一个排好序的新对象。

17. 第五章:建议50:利用模块实现单例模式

a. 不知道为什么要用scalafalcon这种小众语言讲解他们的singleton实现;
b. 利用模块做单例,见下。

1
2
3
4
5
6
7
8
9
10
11
# single.py
class _Singleton(object):
def __init__(self, *args, **kwargs):
do_something()
Singleton = _Singleton()
# other.py
import single
single.Simgleton # 这样保证了单实例

18. 第五章:建议51:用mixin模式让程序更加灵活

Minxin也是一种多重继承的实现方式。ruby的多重继承就是mixin,它规定一个类只能有一个普通父类,但是可以有多个minin的父类。mixin类有几个特点:第一不能实例化,第二不能单独使用。简单的说,minxin类只实现了普通类的功能,不实现普通类的状态(改变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class IPerson(object):
def __init__(self, age):
self.age = 10
# 一个mixin类,实现跑的功能
# python不支持mixin语法,只要mixin类不实现自己特殊的初始化函数即可
class Runable(object):
def run(self):
print 'running'
class Crawl(object):
def crawl(self):
print 'crawl'
class Adult(IPerson, Runable, Crawl):
pass
class Baby(IPerson, Crawl)
pass
adult = Adult(100)
adult.run()
>>> running
adult.crawl()
>>> crawl
baby = Baby(1)
baby.crawl()
>>> crawl

另外,简单的在初始化时传入行为是在python中更加好的办法(而不是动态修改基类),这叫做策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import types
def run(self):
print 'run...'
def crawl(self):
print 'crawl..'
class Person(self):
def __init__(self, age, actions=None):
self.age = age
actions = actions or {}
for name, action in actions:
# 使用types.MethodType将func的类型转变为instancemethod,不转也行
setattr(self, name, types.MethodType(action, self))
adult = Person(25, {'run': run, 'crawl': crawl})
adult.run()
>>> run...
adult.crawl()
>>> crawl...
baby = Person(1, {'crawl': crawl})
baby.crawl()
>>> crawl...

19. 第五章:建议56:理解名字查找机制

其中嵌套作用域的代码缺少outer部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def outer():
var = 'a'
def inner():
global var
var = 'b'
print 'inside inner var is', var
inner()
print 'outside inner var is', var
outer()
>>> inside inner, var is b
>>> outside outer, var is a
var
>>> b

20. 第六章:建议60:区别__getattr____getattribute__

a. __getattr__是一般的属性方法查找函数,当属性方法不在instance.__dict__或者调用方法返回AttributeError时被调用;
b. __getattribute__会接管一切属性方法包括特殊方法的查找,即使当前实例的属性方法存在。

21. 第六章:建议65:熟悉python的迭代器协议

a. 产生器是产生器,迭代器是迭代器。python没有强制类型,一般都过协议(接口)定义行为。产生器只不过是支持迭代器的协议有相应的接口,但不能说产生器是迭代器。
b. groupby的代码示例有误,输出的是一个列表的列表。

1
2
3
4
5
[list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CCC DD
==>
[list(g) for k, g in groupby('AAAABBBCCD')]
>>> [['A', 'A', 'A', 'A'], ['B', 'B', 'B'], ['C', 'C'], ['D']]

21. 第六章:建议67:基于生成器的协程及greenlet

”其中的适时是多久,基本只能静态地使用经验值“。生产-消费模型中一般会使用信号量来解决等待和唤醒问题,不用人为的调用sleep设置睡眠时间,所以也不用”静态地使用经验值“。

22. 第六章:建议68:理解GIL的局限性。

a. “默认情况下每个100个时钟“。python的100个执行指令,但是并不是严格限制,因为有些指令会有跳转等情况导致不能严格的100个指令就让出线程。确切的说是进行一次线程switch,python2.7不保证当前的线程一定会让出,全部由操作系统内核本身决定。

b. GIL存在的主要原因就是GIL会使解释器简洁而且高效。不然的话,需要将全局锁细化到每个需要同步的代码和结构中,大量锁的处理使解释器很难编写,大量锁的竞争等反而使解释器性能下降。简单来说,GIL是粗粒度的锁,比细粒度的锁的优势更大(一种折中方案)。

23. 第八章:建议87:利用set的优势

“1)向list和set中添加元素,…list的耗时约是set的89倍“。这个结论有误,原因在于测试代码中增加了查找操作”x in tmp“,list中的查找是O(n),set是O(1),而不是添加元素的耗时差距。实际上,由于set是哈希表需要计算添加元素的哈希值并且需要调用冲撞函数,一般比list的直接添加元素需要更多额外操作的时间。

1
2
3
4
5
6
7
s = list();
%timeit for i in xrange(1000): s.append(i)
10000 loops, best of 3: 155 µs per loop
s = set();
%timeit for i in xrange(1000): s.add(i)
10000 loops, best of 3: 158 µs per loop