摘要:随着大数据与人工智能时代的到来,Python 近年来颇受程序员喜爱,在 TIOBE 编程语言排行榜中也稳居第一。但这并不说明 Python 毫无缺点,本文作者就将盘点一些 Python 的“迷惑性为”。
原文链接:https://medium.com/geekculture/why-python-still-is-a-mess-1f7bf5bca281
声明:本文为 CSDN 翻译,未经授权,禁止转载。
长期以来,Python一直自诩是最适合新手程序员的语言之一。话虽没错,但这并不意味着编程新手不会对Python的一些行为感到困惑。
举个例子,动态类型。你无需单独编写一行代码来定义变量的类型,Python能够自行分辨,乍一看之下,这似乎很神奇。感觉这样编程速度更快。
然而,就因为少了一行变量定义,整个项目在运行结束之前就有可能崩溃。
说句公道话,许多其他编程语言也使用动态类型。但对于Python而言,这只是一系列噩梦的开始。
几年前,我想在同事编写的一个软件的基础之上,进行二次开发。我知道该软件的基本思想,我的同事甚至写了一篇论文作为该软件的文档。
但是,我仍然需要阅读数千行 Python 代码,才能搞清楚各个部分在干什么,以及我可以将新功能放到哪里。然而,就在这个过程中,我遇到了很大的问题……
纵观整个代码库,变量声明到处都是。为了搞清楚每个变量的用途,我不得不搜索整个文件,甚至是整个项目。
此外,还有各种各样的复杂情况,比如函数的某个参数的名字和调用该函数时使用的变量完全不同,或者一个变量与某个类紧密结合,而该类又和另一个类中的某个变量交织在一起……诸如此类的事情层出不穷。
很多人都有类似的感觉,有人就曾表示显式变量声明优于隐式(参考链接:https://peps.python.org/pep-0020/)。但是,在Python中隐式变量声明比比皆是,尤其是在大型项目中。
在 Python 中,定义函数的时候可以指定可选参数,即不需要明确指定的参数。如下所示:
def add_five(a, b=0):
return a + b + 5
通过这个简单的示例可以看出,在调用函数时,无论指定一个参数还是两个参数都可以:
add_five(3) # returns 8
add_five(3,4) # returns 12
之所以会出现这种现象,是因为表达式b=0定义了b是一个整数,而整数是不可变的。再看看下面这个例子:
def add_element(list=[]):
list.append("foo")
return list
add_element() # returns ["foo"], as expected
发现问题了吗?再执行一次会怎么样?
add_element()
因为这里的list已经存在,即["foo"],而Python会继续向这个列表添加新东西。这是因为列表与整数不同,是可变类型。
我不禁想起一句话:“疯子就是不断重复同一件事,却期待不同的结果。”(经考证,这句话不是爱因斯坦说的)。我想说,Python + 可选参数 + 可变对象 = 疯子。
如果你认为上述问题仅限于可变对象作为可选参数的时候,那你就大错特错了。
相信你也使用Python编写面向对象的代码,在Python代码中类无处不在。而类最实用的特性之一便是:继承。
简单来说,如果父类具有某些属性,子类就可以继承这些属性。如下所示:
class parent(object):
x = 1
class firstchild(parent):
pass
class secondchild(parent):
pass
print(parent.x, firstchild.x, secondchild.x) # returns 1 1 1
注意,这段代码写得并不好,不要复制到实际的项目中。关键在于,子类继承了 x = 1,因此我们可以获取子类的这个属性,得到的结果与父类相同。
如果我们修改某个子类的x属性,那么理应说变化的只有这个子类。就好像孩子染发不可能改变父母亲或兄弟姐妹的发色。代码如下:
firstchild.x = 2
print(parent.x, firstchild.x, secondchild.x) # returns 1 2 1
如果这时父母染发,孩子的发色会变吗?不会变,对不对?
parent.x = 3
print(parent.x, firstchild.x, secondchild.x) # returns 3 2 3
出现这个结果是因为Python的方法解析顺序(http://python-history.blogspot.com/2010/06/method-resolution-order.html)。简单来说,只要没有另行说明,子类就会继承父类拥有的一切。也就是说,在Python的世界里,如果你不提前抗议,那么你妈妈在染头发的时候,会顺带连你的头发一起染了。
我个人已经因为这个问题多次栽跟头。
在Python中,函数内部定义的变量无法在函数外部使用,这是因为超出了作用域:
def myfunction(number):
basenumber = 2
return basenumber*number
basenumber
# Oh no! This is the error:
Traceback (most recent call last):
"<stdin>", line 1, in <module> File
'basenumber' is not defined NameError: name
这部分完全符合直觉,我栽跟头也不是因为这部分代码。
但是反过来呢?我的意思是,如果我在函数外部定义一个变量,然后在函数内部引用它呢?
x = 2
def add_5():
x = x + 5
print(x)
add_5()
# Oh dear...
Traceback (most recent call last):
"<stdin>", line 1, in <module> File
"<stdin>", line 2, in add_y File
local variable 'x' referenced before assignment UnboundLocalError:
这就很奇怪了,不是吗?我们生活在一个有树的世界里,虽然平时我们住在房子里,但肯定也知道树长什么样子,对不对?(树是 x,房子是 add_5(),我们是 5……)
有好多次,我在某个类中调用另一个类中定义的函数,就遇到了错误。我花了很长一段时间才找到问题的根源。
其背后的基本思想是,函数内部的 x 与外部的 x 是不同的,所以你不能在外部调用它。
幸运的是,这个问题有一个简单的解决方案,即在 x 之前加一个global,让x变成全局变量!
x = 2
def add_5():
global x
x = x + 5
print(x)
add_5() # works!
所以说,如果你认为作用域的目的仅仅是保护函数内部的变量不被外部干扰,那就大错特错了。在Python中,局部作用域也无法访问外部。
请看如下代码:
mynumbers = [x for x in range(10)]
# this is [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for x in range(len(mynumbers)):
if mynumbers[x]%3 == 0:
mynumbers.remove(mynumbers[x])
这个循环出错,是因为循环在迭代的过程中不断删除列表中的元素。因此,列表不断缩短,循环不可能到达第10个元素,因为它不存在了!
有一种解决方法是,为你想删除的所有元素统一分配一个值,然后在循环结束后删除它们。
此外,似乎还有一种更好的解决方式:
mynumbers = [x for x in range(10) if x%3 != 0]
只需要一行代码!
请注意,在上面的示例中,我们使用了 Python 的列表推导式来调用列表。
列表推导式指的是方括号([])中的表达式,一般都是循环的缩写形式。列表推导式通常比常规循环更快,因此非常适合处理大型数据集。
在这个示例中,我们添加了一个 if 子句来告诉列表推导式:不应包含可被 3 整除的数字。
这个问题与前面的几个不同,我不认为这是Python的迷惑行为,相反我认为这种处理很聪明,尽管初学者理解起来会有些困难。
实际上,我们对Python的不满不止是编写代码的痛苦,别忘了,以前Python的执行速度非常慢,比大多数其他语言慢 2~10 倍。
现在情况已经好很多了。例如,现在Numpy 包能够非常快速地处理列表、矩阵等。
Python的多线程处理也变得更加容易了。你可以使用计算机上的多个内核,我曾在 20 个内核上运行进程,为我节省了数周的计算时间。
此外,在过去几年中,随着机器学习的蓬勃发展,Python 也表现出了进一步的发展空间。Pytorch 和 Tensorflow 等包的出现推动了Python的采用,而其他语言也正在努力中。
虽然,多年来Python在不断进步,但这并不能保证Python未来的发展会一帆风顺。Python语言的学习并没有那么简单,请多加小心。