Python中的十大编码错误及避免这些错误方法

39 阅读9分钟

Python很灵活,很有趣,也很容易学习,但是和其他编程语言一样,如果你不了解Python的怪癖,有一些常见的错误会让你头疼。在这篇文章中,我们将通过向你介绍10个最常见的错误来帮助你在未来节省时间,你可以轻松地避免这些错误。

注意: 如果你没有安装最近的Python拷贝来尝试本文中的例子,你可以 从ActiveState平台上免费下载一个 Windows、Linux或macOS版本。或者更好、更安全的入门方式是创建一个只有你需要的软件包的Python 环境

无主的脚本

让我们从一个初学者的错误开始,如果不及时发现,会给你带来一些问题。由于 Python 是一种脚本语言,你可以定义可以在 REPL 模式下调用的函数。比如说:

def super_fun():

   print("Hello, I will give you super powers")

super_fun()

这段简单的代码 在CLI python no_main_func.py中 调用 super_fun 函数时 将执行 它。但是,当你想把这些代码作为一个模块在笔记本中重用时,例如,会发生什么呢?

coding mistakes python

正如你所看到的, 当你导入你的脚本时, super_fun 会自动执行。这是一个无害的例子,但是想象一下,如果你的函数执行了一些昂贵的计算操作,或者调用了一个产生多个线程的进程。你不会想在导入时自动运行这些操作,那么你如何防止这种情况发生呢?

一种方法是使用简单的 标准__main__ 脚本执行范围的修改版本 ,如下所示:

def super_fun():

   print("Hello, I will give you super powers")


if __name__ == "__main__":

   # execute only if run as a script

   super_fun()

使用这段代码,你会得到与之前从CLI调用脚本文件时相同的结果,但这一次,当你把它作为模块导入时,你不会得到意外的行为。

浮点数据类型

初学者的另一个常见问题是 Python 中的浮点管理。新手们常常错误地认为浮点是一种简单的类型。例如,下面的代码显示了比较简单类型(如int数)与浮点类型的标识符之间的区别。

>>> a = 10

>>> b = 10

>>>

>>> id(a) == id(b)

True

>>> c = 10.0

>>> d = 10.0

>>>

>>> id(c) == id(d)

False

>>> print(id(a), id(b))

9788896 9788896

>>> print(id(c), id(d))

140538411157584 140538410559728
    

浮点类型也有一个微妙但重要的特点:其内部表示。下面的代码使用了一个简单的算术运算,应该很容易解决,但是比较运算符 == 给你带来了意外的结果。

>>> a = (0.3 * 3) + 0.1

>>> b = 1.0

>>> a == b

False

出现意外结果的原因是,浮点运算由于其内部表示方式的不同,会有微小的(甚至是显著的)差异。下面的函数允许你使用它们之间差异的绝对值来比较浮点数。

def super_fun(a:float, b:float):

   return True if abs(a-b) < 1e-9 else False


if __name__ == "__main__":

   # execute only if run as a script

   print(super_fun((0.3*3 + 0.1),1.0))

布尔的困惑

什么应该被认为是布尔 "真 "值的定义是几种编程语言混乱的主要来源,Python也不例外。考虑一下下面的比较。

>>> 0 == False

True

>>> 0.0 == False

True

>>> [] == False

False

>>> {} == False

False

>>> set() == False

False

>>> bool(None) == False

True

>>> None == False

False

>>> None == True

False

正如你所看到的,任何数字数据类型的零值都被认为是 "假的",但像列表、集合或字典这样的空集合就不是。请记住,"无 "与 "真 "和 "假 "是不同的。这可能是有问题的,因为一个变量可能是未定义的,但后来被用于比较,会产生意想不到的结果。

不可阻挡的脚本

当初学者想在他们的脚本中执行无限循环,同时又想保留停止循环的能力时,就会出现一种稍微不同的问题。例如,请看下面这个循环。

while True:

   try:

       print("Run Forest, run")

       time.sleep(5)

   except:

       print("Forrest cannot stop")


Run Forest, run

^CStop Forrest

Run Forest, run

^CStop Forrest

Run Forest, run

Run Forest, run

你可能已经注意到,即使是^C的力量也不足以停止这个循环。但为什么呢?这是因为 try/except 块甚至捕捉到了 KeyboardInterrupt。如果你想只捕获 异常,你必须明确说明这一点。

while True:

   try:

       print("Run Forest, run")

       time.sleep(5)

   except Exception:

       print("Forrest cannot stop")


Run Forest, run

Run Forest, run

Run Forest, run

^CTraceback (most recent call last):

 File "with_main_func.py", line 6, in <module>

   time.sleep(5)

KeyboardInterrupt

但是 KeyboardInterrupt 也继承自 BaseException,所以你可以很容易地捕获它并管理它。

while True:

   try:

       print("Run Forest, run")

       time.sleep(5)

   except Exception:

       print("Forrest cannot stop")

   except KeyboardInterrupt:

       print("Ok, Forrest will stop now")

       exit(0)


Run Forest, run

Run Forest, run

^COk, Forrest will stop now

模块名称冲突

在流行的编程网站的讨论区中,有一个问题不时地浮现出来,那就是与 Python 模块名称有关的错误。例如,让我们想象一下,你需要解决一个复杂的数学问题,所以你创建了一个 math.py 脚本,代码如下:

from math import abs

def complicated_calculation(a,b):

   return abs(a - b) > 0


if __name__ == "__main__":

  # execute only if run as a script

  complicated_calculation()

如果你运行这个脚本,它将给你一个像这样的堆栈跟踪:

$ python3 math.py

Traceback (most recent call last):

 File "math.py", line 1, in <module>

   from math import abs

ImportError: cannot import name 'abs' from 'math' (unknown location)

这应该是显而易见的:你以一种与标准库之一冲突的方式命名了你自己的模块。有时,由于通常为非琐碎项目建立的依赖树,这可能更加棘手。

为了避免这类命名问题,你可以查看这个 资源 ,了解绝对和相对模块路径导入。

可变函数参数

有时,误用的可变类型会导致意外的行为。例如,考虑下面的片段。

def list_init(alist=[]):

   alist.append('Initialize with a value')

   return alist


if __name__ == "__main__":

  # execute only if run as a script

  a = list_init()

  print(id(a), a)

  b = list_init()

  print(id(b), b)

  c = list_init()

  print(id(c), c)

你可能认为 list_init 函数 的目的 是增加一个新的缺省值,所以在没有参数的情况下调用它 应该 返回一个长度为1的列表。 然而,结果会让你吃惊。

$ python3 mutable_args.py

140082369213568 ['Initialize with a value']

140082369213568 ['Initialize with a value', 'Initialize with a value']

140082369213568 ['Initialize with a value', 'Initialize with a value', 'Initialize with a value']

发生了什么事?列表类型是可变的,考虑到函数参数的缺省值只在函数在 Python 中定义时被评估,空列表将在随后的调用中被引用。

幸运的是,一个简单的改变可以得到你想要的行为。

def list_init(alist=None):

   if not alist:

       alist = []


   alist.append('Initialize with a value')

   return alist


if __name__ == "__main__":

  # execute only if run as a script

  a = list_init()

  print(id(a), a)

  b = list_init()

  print(id(b), b)

  c = list_init()

  print(id(c), c)

迭代过程中的列表突变

许多开发者希望在迭代一个集合时对其进行突变。例如,想象一下手动过滤一个列表,如下所示:

list_nums = list(range(16))

for idx in range(len(list_nums)):

   n = list_nums[idx]

   if n % 3 == 0:

       del list_nums[idx]

print(list_nums)

这将产生一个像这样的堆栈跟踪:

$ python3 list_mutation.py

Traceback (most recent call last):

 File "math.py", line 3, in <module>

   n = list_nums[idx]

IndexError: list index out of range

幸运的是,Python 给你提供了几种技术来完成这样的事情,而不需要写很多代码。例如,你可以这样过滤一个列表。

list_nums = list(range(16))

list_nums = [n for n in list_nums if n % 3 !=0]

print(list_nums)

列表 (和字典理解一起) 是处理集合的最好的方法之一。

引用、拷贝和深度拷贝

许多程序员在修改本应是其他变量的独立副本的变量时都很纠结。这种情况发生在你 认为 你已经创建了一个变量的独立副本,但实际上你只是创建了一个指向它的指针。

例如,当你 以如下方式使用赋值运算符 =时,你可以得到一个变量的引用 。

Python 3.8.10 (default, Jun  2 2021, 10:49:15)

[GCC 9.4.0] on linux

Type "help", "copyright", "credits" or "license" for more information.

>>> d1 = {'k':[1,2,3]}

>>> d2 = d1

>>> print('d1 and d2 points to the same object',id(d1), id(d2))

d1 and d2 points to the same object 140265839379264 140265839379264

正如你所看到的,字典 d1 和 d2 有相同的 id。

现在让我们试着用copy()方法获得一个变量的独立拷贝。

>>> d2['k'].append(99)

>>> d3 = d1.copy()

>>> print('d3 is a different object but its contents points to d1 contents',id(d3), d1, d2, d3)

d3 is a different object but its contents points to d1 contents 140674662142848 {'k': [1, 2, 3, 99]} {'k': [1, 2, 3, 99]} {'k': [1, 2, 3, 99]}

在这种情况下,你可能认为修改 d2 不会影响 d1,但实际上它们都会被修改,因为它们共享同一个引用。奇怪的是, d3 也会被修改,尽管它是一个不同的引用。这是因为 d3 是 d1 的 一个 浅层拷贝,这意味着它的内容指向相同的东西。

要得到一个完全不同的变量实例,并有自己的内容,然后你可以修改它,你必须使用一个 深度拷贝

>>> import copy

>>> d4 = copy.deepcopy(d1)

>>> d3['k'].append(199)

>>> print('d4 is a different object AND its contents is also independent',id(d4), d1, d2, d3, d4)

d4 is a different object AND its contents is also independent 139809259201088 {'k': [1, 2, 3, 99, 199]} {'k': [1, 2, 3, 99, 199]} {'k': [1, 2, 3, 99, 199]} {'k': [1, 2, 3, 99]}

注意, d4的内容 与 d1、 d2和 d3的 字典 不 一样 。

类变量

面向对象编程的目的是以模仿现实世界的方式来构造问题,但对于没有经验的程序员来说,它可能显得很麻烦。最常见的问题之一是理解 "类 "和 "实例 "变量之间的区别。

例如,考虑下面的代码:

class Local:

   motto = 'Think globally'

   actions = []


instance1 = Local()

instance2 = Local()

instance2.motto = 'Act locally'

instance2.actions.append('Reuse')

print( instance1.motto, instance2.motto )

print( instance1.actions, instance2.actions )

结果可能是意想不到的:

$ python3 class_vars.py

Think globally Act locally

['Reuse'] ['Reuse']

你可能期望全局属性 格言 对所有实例都有所改变,因为列表 中的Actions 就是这样 ,但是当 Actions 指向所有实例的同一个引用时, 一旦你改变了 格言 字符串,它就会被复制成一个实例属性。

一般来说,类变量通常是不必要的,而且通常不鼓励使用。

通过引用和值的函数参数

Python 在函数和方法中使用参数有一种特殊的方式。从像 Java 或 C++ 这样的语言转到 Python 的程序员可能会对解释器处理参数的方式有一些不理解。

例如,考虑下面的例子。

def mutate_args(a:str, b:int, c:float, d:list):

   a += " mutated"

   b += 1

   c += 1.0

   d.append('mutated')

   print('After mutation, inside func')

   print(id(a), id(b), id(c), id(d) )

a = "String"

b = 0

c = 0.0

d = ['String']


print( id(a), id(b), id(c), id(d) )

mutate_args(a,b,c,d)

print( a,b,c,d )

print( id(a), id(b), id(c), id(d) )

输出显示了 每个变量的ID 。注意,除了对列表的引用,函数内部的变量与原始变量不同。

在下面的例子中,被引用的变量按照预期被突变了,而引用则被保留在函数内部。

$ python3 args.py

139733890106736 9788576 139733890064464 139733890004160

After mutation, inside func

139733890107376 9788608 139733891541072 139733890004160

String 0 0.0 ['String', 'mutated']

139733890106736 9788576 139733890064464 139733890004160

你应该注意遵循最佳实践,如返回几个值或使用对象属性,以避免在突变函数参数时出现混乱。要想更深入地了解通过引用传递参数的问题,请查看这个 链接

总结

Python 是一种高级语言,一旦你理解了它的一些怪癖,就会觉得很有趣。由于 Python 非常适用于快速而简单地解决许多日常的编程情况,所以值得将你的头脑与这些怪癖联系起来。

但是为了避免在调试那些乍一看还不错的代码时的头痛,你必须了解Python的设计元素。这篇文章对初学者经常犯的十大错误进行了介绍,并给了你如何避免这些错误的提示。但是还有很多其他资源可以帮助你入门,包括

多年来,Python的另一个问题是包管理,这个问题一直持续到现在,而且变得越来越混乱。软件包管理本身就是一个难题,而且它已经被扩展到包括环境管理。尽管很复杂,但要把它搞好是很重要的。

不幸的是,对于 Python 来说,历史上很容易出错。这也是Python具有这么多不同的包管理器的关键原因之一。

在开发过程中,为了解决环境中的问题而把时间沉入依赖性地狱是浪费时间的。因此,许多开发者选择了一个复杂的解决方案,尽可能多地完成繁重的工作,使他们能够自由地专注于编码。

在现代技术世界中,安全的编码方式应该是最简单的编码方式。但是,在你的项目中引入一个依赖性,不可避免地会带来其他的依赖性,从而产生多米诺骨牌效应,使发现安全漏洞成为挑战。

即使你发现了这些漏洞,除非它们是关键漏洞,否则解决它们的时间和精力意味着它们很少被解决,从而使你的开发和测试环境暴露于网络攻击。

如果你想消除依赖地狱,并在开发和测试中创建更安全的代码,同时又不拖累你的冲刺,我会推荐一个能够解决所有其他限制的依赖管理器。看看ActiveState平台吧。