关于 Python,一些不得不吐槽的“迷惑行为”

43e08ea090c8185ab8098d5f16875a9b.gif

摘要:随着大数据与人工智能时代的到来,Python 近年来颇受程序员喜爱,在 TIOBE 编程语言排行榜中也稳居第一。但这并不说明 Python 毫无缺点,本文作者就将盘点一些 Python 的“迷惑性为”。

原文链接:https://medium.com/geekculture/why-python-still-is-a-mess-1f7bf5bca281

声明:本文为 CSDN 翻译,未经授权,禁止转载。
作者 | Ari Joury

译者 | 弯月

出品 | CSDN(ID:CSDNnews)

长期以来,Python一直自诩是最适合新手程序员的语言之一。话虽没错,但这并不意味着编程新手不会对Python的一些行为感到困惑。

举个例子,动态类型。你无需单独编写一行代码来定义变量的类型,Python能够自行分辨,乍一看之下,这似乎很神奇。感觉这样编程速度更快。

然而,就因为少了一行变量定义,整个项目在运行结束之前就有可能崩溃。

说句公道话,许多其他编程语言也使用动态类型。但对于Python而言,这只是一系列噩梦的开始。

关于 Python,一些不得不吐槽的“迷惑行为”_第1张图片

隐式的变量声明会影响阅读代码

几年前,我想在同事编写的一个软件的基础之上,进行二次开发。我知道该软件的基本思想,我的同事甚至写了一篇论文作为该软件的文档。

但是,我仍然需要阅读数千行 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() # returns ["foo", "foo"]! wtf!

因为这里的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):
# File "", line 1, in 
# NameError: name 'basenumber' is not defined

这部分完全符合直觉,我栽跟头也不是因为这部分代码。

但是反过来呢?我的意思是,如果我在函数外部定义一个变量,然后在函数内部引用它呢?

x = 2
def add_5():
x = x + 5
print(x)
add_5()
## Oh dear...
# Traceback (most recent call last):
# File "", line 1, in 
# File "", line 2, in add_y
# UnboundLocalError: local variable 'x' referenced before assignment

这就很奇怪了,不是吗?我们生活在一个有树的世界里,虽然平时我们住在房子里,但肯定也知道树长什么样子,对不对?(树是 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])
## Ew! 
# Traceback (most recent call last):
# File "", line 2, in 
# IndexError: list index out of range

这个循环出错,是因为循环在迭代的过程中不断删除列表中的元素。因此,列表不断缩短,循环不可能到达第10个元素,因为它不存在了!

有一种解决方法是,为你想删除的所有元素统一分配一个值,然后在循环结束后删除它们。

此外,似乎还有一种更好的解决方式:

mynumbers = [x for x in range(10) if x%3 != 0]
# that's what we wanted! [1, 2, 4, 5, 7, 8]

只需要一行代码!

请注意,在上面的示例中,我们使用了 Python 的列表推导式来调用列表。

列表推导式指的是方括号([])中的表达式,一般都是循环的缩写形式。列表推导式通常比常规循环更快,因此非常适合处理大型数据集。

在这个示例中,我们添加了一个 if 子句来告诉列表推导式:不应包含可被 3 整除的数字。

这个问题与前面的几个不同,我不认为这是Python的迷惑行为,相反我认为这种处理很聪明,尽管初学者理解起来会有些困难。

总结

实际上,我们对Python的不满不止是编写代码的痛苦,别忘了,以前Python的执行速度非常慢,比大多数其他语言慢 2~10 倍。

现在情况已经好很多了。例如,现在Numpy 包能够非常快速地处理列表、矩阵等。

Python的多线程处理也变得更加容易了。你可以使用计算机上的多个内核,我曾在 20 个内核上运行进程,为我节省了数周的计算时间。

此外,在过去几年中,随着机器学习的蓬勃发展,Python 也表现出了进一步的发展空间。Pytorch 和 Tensorflow 等包的出现推动了Python的采用,而其他语言也正在努力中。

虽然,多年来Python在不断进步,但这并不能保证Python未来的发展会一帆风顺。Python语言的学习并没有那么简单,请多加小心。

— 推荐阅读 —

关于 Python,一些不得不吐槽的“迷惑行为”_第2张图片

你可能感兴趣的:(列表,编程语言,python,java,人工智能)