译自:https://www.tutorialdocs.com/article/9-worst-python-practices.html
目录
最近我一直在检查旧系统,其中一些由于编码习惯不良而变得很糟糕。我还写了一段不好的代码,导致服务器负载飙升,所以我想总结一下糟糕的 Python 编程习惯,提醒自己远离这些“最糟糕的做法”。
在下面的例子中,一些会导致性能问题,一些会导致隐藏的错误或未来的维护和重构的困难,而另一些则是我认为不够pythonic。
使用可变对象作为默认参数
这种糟糕的编程习惯应该在各种技术文章中都可以看到。
让我们先看一下错误的演示:
def use_mutable_default_param(idx=0, ids=[]):
ids.append(idx)
print(idx)
print(ids)
use_mutable_default_param(idx=1)
use_mutable_default_param(idx=2)
输出为:
1
[1]
2
[1, 2]
最关键的原因是:
- 函数本身也是一个对象,默认参数被绑定到函数对象。
append
方法将会直接修改对象,因此下次调用该函数时,绑定的默认参数不再为空列表。
正确的操作如下:
def donot_use_mutable_default_param(idx=0, ids=None):
if ids is None:
ids = []
ids.append(idx)
print(idx)
print(ids)
在 try...except 语句中不指定异常类型
尽管在 Python 中使用 try...except
不会导致严重的性能问题,但直接捕获所有类型的异常通常会掩盖其他错误并导致难以跟踪的错误。
通常,try...except
应尽可能少地使用,以便在开发阶段的早期发现问题。如果要使用 try...except
,则应尽可能指定要捕获的特定异常,并通过 except 语句将异常信息写入日志,或者在处理后直接 raise。
关于字典的冗余代码
我经常可以看到这样的代码:
d = {}
datas = [1, 2, 3, 4, 2, 3, 4, 1, 5]
for k in datas:
if k not in d:
d[k] = 0
d[k] += 1
实际上,可以使用数据结构 collections.defaultdict
更简单、更优雅地实现这样的功能:
default_d = defaultdict(lambda: 0)
datas = [1, 2, 3, 4, 2, 3, 4, 1, 5]
for k in datas:
default_d[k] += 1
再看如下代码:
# d is a dict
if 'list' not in d:
d['list'] = []
d['list'].append(x)
可以使用一行代码替换:
# d is a dict
d.setdefault('list', []).append(x)
同样,以下两种编程方式具有强烈的 C 风格:
# d is a dict
for k in d:
v = d[k]
# do something
# l is a list
for i in len(l):
v = l[i]
# do something
你最好以更 pythonic 的方式编写:
# d is a dict
for k, v in d.iteritems():
# do something
pass
# l is a list
for i, v in enumerate(l):
# do something
pass
实际上,enumerate
还有另一个参数,表示序列号的起始位置。如果你希望序列号从 1 开始,则可以使用 enumerate(l, 1)
。
使用 flag 变量而不是 for ... else
同样,这种代码很常见:
search_list = ['Jone', 'Aric', 'Luise', 'Frank', 'Wey']
found = False
for s in search_list:
if s.startswith('C'):
found = True
# do something when found
print('Found')
break
if not found:
# do something when not found
print('Not found')
事实上,使用 for...else
会更优雅:
search_list = ['Jone', 'Aric', 'Luise', 'Frank', 'Wey']
for s in search_list:
if s.startswith('C'):
# do something when found
print('Found')
break
else:
# do something when not found
print('Not found')
过度使用元组解包
在 Python 中,允许对元组类型执行解包操作:
# human = ('James', 180, 32)
name, height, age = human
这种做法非常酷,而且比编写 name=human[0]
要聪明得多。然而,它经常被滥用。
如果你之后需要在 human
中插入性别数据 sex
,那么所有的解包操作都需要修改,即使 sex
不会在某些逻辑中使用。
# human = ('James', 180, 32)
name, height, age, _ = human
# or
# name, height, age, sex = human
有几种方法可以解决这个问题:
- 使用
name=human[0]
编程方式,然后在需要性别信息的地方插入sex=human[3]
- 使用
dict
代表human
- 使用
namedtuple
# human = namedtuple('human', ['name', 'height', 'age', 'sex'])
h = human('James', 180, 32, 0)
# then you can use h.name, h.sex and so on everywhere.
在任何地方使用import *
Import*
是一种惰性行为,不仅会污染当前的命名空间,还会使代码检查工具(如 pyflakes)无效。在随后查看代码或调试的过程中,通常很难从一堆 import*
中找出第三方函数的来源。
文件操作
不要使用 f = open('filename')
进行文件操作。使用 with open('filename') as f
让上下文管理器帮助你处理乱七八糟的东西,例如关闭文件。
使用 class.name 确定类型
我遇到过一个错误:为了实现一个特定的函数,我编写了一个新的类 B(A),并在 B 中重载了 A 中的几个函数。整个实现很简单,但是 A 的某些函数不起作用。最后我发现原因是在某些逻辑代码中,我使用 entity.__class__.__name__ == 'A'
进行判断。
除非要限制继承层次结构中的当前类型(即屏蔽将来可能出现的子类),否则不要使用 __class__.__name__
,而应使用内置函数 isinstance
。毕竟,这两个变量的名称中有很多下划线,意味着不建议使用它们。
循环内有多层函数调用
循环内的多层函数调用带来以下两个隐藏的风险:
- Python 中没有内联函数,因此函数调用会产生一定的开销。特别是当逻辑简单时,开销的比例将是相当大的。
- 更重要的是,当你稍后维护代码时,你可能会忽略在循环中调用了该函数。因此,在函数内部,你将倾向于添加一些具有更大开销但不必每次都调用的函数,例如
t
ime.localtime()
。如果它是一个简单的循环,我认为大多数程序员都会将time.localtime()
编写在循环之外,但如果引入多层函数调用则不会。
所以我建议如果不是特别复杂的逻辑,它应该直接写在循环内部而不是使用函数调用。如果必须包装一层函数调用,则应该在函数的命名或注释中提示后续维护者:此函数将在循环内使用。
Python 是一种非常容易上手的语言。严格的缩进要求和丰富的内置数据类型使得大多数 Python 代码都能做得很好。但是,在 Python 中编写错误的代码也很容易。以上列出的只是不良做法的一小部分。如果有任何遗漏,请随时告诉我。