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 函数时 将执行 它。但是,当你想把这些代码作为一个模块在笔记本中重用时,例如,会发生什么呢?
正如你所看到的, 当你导入你的脚本时, 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的设计元素。这篇文章对初学者经常犯的十大错误进行了介绍,并给了你如何避免这些错误的提示。但是还有很多其他资源可以帮助你入门,包括
- Real Python 的 Python 基础知识书
- Steve Campbell的 Python初学者教程
- 机器学习、Pandas和Tkinter的Python小抄
多年来,Python的另一个问题是包管理,这个问题一直持续到现在,而且变得越来越混乱。软件包管理本身就是一个难题,而且它已经被扩展到包括环境管理。尽管很复杂,但要把它搞好是很重要的。
不幸的是,对于 Python 来说,历史上很容易出错。这也是Python具有这么多不同的包管理器的关键原因之一。
在开发过程中,为了解决环境中的问题而把时间沉入依赖性地狱是浪费时间的。因此,许多开发者选择了一个复杂的解决方案,尽可能多地完成繁重的工作,使他们能够自由地专注于编码。
在现代技术世界中,安全的编码方式应该是最简单的编码方式。但是,在你的项目中引入一个依赖性,不可避免地会带来其他的依赖性,从而产生多米诺骨牌效应,使发现安全漏洞成为挑战。
即使你发现了这些漏洞,除非它们是关键漏洞,否则解决它们的时间和精力意味着它们很少被解决,从而使你的开发和测试环境暴露于网络攻击。
如果你想消除依赖地狱,并在开发和测试中创建更安全的代码,同时又不拖累你的冲刺,我会推荐一个能够解决所有其他限制的依赖管理器。看看ActiveState平台吧。