9 个最糟糕的 Python 编程习惯

译自:https://www.tutorialdocs.com/article/9-worst-python-practices.html

目录

使用可变对象作为默认参数

在 try...except 语句中不指定异常类型

关于字典的冗余代码

使用 flag 变量而不是 for ... else

过度使用元组解包

在任何地方使用import *

文件操作

使用 class.name 确定类型

循环内有多层函数调用


最近我一直在检查旧系统,其中一些由于编码习惯不良而变得很糟糕。我还写了一段不好的代码,导致服务器负载飙升,所以我想总结一下糟糕的 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]

最关键的原因是:

  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

有几种方法可以解决这个问题:

  1. 使用 name=human[0] 编程方式,然后在需要性别信息的地方插入 sex=human[3] 
  2. 使用 dict 代表 human
  3. 使用 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。毕竟,这两个变量的名称中有很多下划线,意味着不建议使用它们。


循环内有多层函数调用

循环内的多层函数调用带来以下两个隐藏的风险:

  1. Python 中没有内联函数,因此函数调用会产生一定的开销。特别是当逻辑简单时,开销的比例将是相当大的。
  2. 更重要的是,当你稍后维护代码时,你可能会忽略在循环中调用了该函数。因此,在函数内部,你将倾向于添加一些具有更大开销但不必每次都调用的函数,例如 time.localtime()。如果它是一个简单的循环,我认为大多数程序员都会将 time.localtime() 编写在循环之外,但如果引入多层函数调用则不会。

所以我建议如果不是特别复杂的逻辑,它应该直接写在循环内部而不是使用函数调用。如果必须包装一层函数调用,则应该在函数的命名或注释中提示后续维护者:此函数将在循环内使用。

Python 是一种非常容易上手的语言。严格的缩进要求和丰富的内置数据类型使得大多数 Python 代码都能做得很好。但是,在 Python 中编写错误的代码也很容易。以上列出的只是不良做法的一小部分。如果有任何遗漏,请随时告诉我。

猜你喜欢

转载自blog.csdn.net/qq_20084101/article/details/82495974