坎德人的小包包

欧剃,游荡的坎德人,在他的旅途中收集了许许多多有趣的东西。

发表日期:2020-03-02

Python 新人需要避免的 4 条常见错误

—— 我用最崎岖的方式学到了教训,希望你不用重走这条弯路。

作者:Eden Au


图片来源:Unsplash,摄影 Jamie Street

“面对现实吧,学编程不能有小聪明。”

上面这句话,有许多人觉得有道理。而我曾对它不屑一顾。

这是因为,在学习各种不同的编程语言时,我总能发现一些微妙的方法,来完成我想做的任何事情。我曾认为我能掌控一切。然而我错了。

你能在你的代码里做任何事,但你不应该任意乱来。

我很快就意识到,我的那些“微妙”的操作其实都是些糟糕的垃圾代码。但明明能得出正确的运行结果,为啥说是垃圾代码呢?我曾习惯于这些糟糕的编程“技巧”,直到我被一个复杂的项目狠狠摆了一道。我算是用最笨的办法学到了这个教训。

在实际开始介绍这 4 条常见的错误做法之前,我希望你已经对接下来要涉及的 Python 内置特性有了一些大概的了解。

让我们开始吧!

错误 1:不使用迭代器

基本上每个刚学 Python 的新人都干过这事。这和 ta 之前是否学过其他编程语言还没什么关系,谁都会犯错。

举个例子,假如手上有个列表 list_,你要怎么用 for 循环来按顺序读取列表中的每一个元素呢?你看,我觉得,既然 Python 中的列表是有序的,我就可以通过它的索引 i 来读取列表中的第 i 个元素,比如 list_[i]。那么,接下来我就用一个循环变量,在 for 循环中从 0 遍历到列表的总长度 len(list_),读取每一个值:

for i in range(len(list_)):
    print(list_[i])

它能正常工作。这些代码运行起来没有问题。甚至在其他一些编程语言(比如 C 语言)中,这还是标准的 for 循环格式。

但在 Python 里,我们实际上有更好的做法。

咋整?

知道吗,在 Python 里,列表对象本身就是可迭代的。使用这个特性,我们可以用 for ... in ... 语句,构建一个简洁易懂的循环:

for element in list_:
    print(element)

图片来源:Unsplash,摄影 The Creative Exchange

如果你想要在 for 循环中并行遍历多个列表对象,你可以使用 zip 函数,而如果你坚持要在遍历可迭代对象的时候获取对应的索引号(例如计数),你可以使用 enumerate 函数。

错误 2:滥用全局变量

全局变量是在主代码块中以全局作用域声明的变量,而局部变量则是在某个函数中以局部作用域声明的变量。使用 global 关键字,你可以在函数内部改变全局变量的值。比如下面这个例子:

a = 1 # 在顶层定义的一个变量 a

def increment():
    a += 1
    return a

def increment2():
    global a # 将全局变量 “a” 引入函数内部以供修改
    a += 1 
    return a
  
increment()  
# 返回错误信息:UnboundLocalError: local variable 'a' referenced before assignment
increment2() 
# 返回: 2

许多初学者喜欢这样操作,使用 global 关键字,似乎可以省下许多在函数间传递参数的麻烦事。然而这是不对的,这让你难以追踪函数的行为。

同样,滥用全局变量还会让你的调试工作难上加难。每个函数都应该像一个独立的盒子,有着明确的功能,并且可以被重复使用。会修改全局变量的函数可能会给主脚本带来很难发现的副作用,这会让你的代码变成一团乱麻,让你无法进行调试。

在一个局部函数里修改全局变量是一个非常糟糕的编程做法。你应当将所需的变量作为参数传给函数,并且在函数结尾返回一个值给主脚本。

图片来源:Unsplash,摄影 Vladislav Klapin

*注:别把全局变量和全局常量搞混了,在大部分情况下,定义全局常量都是个很好的习惯。

错误 3:不理解可变对象

对于 Python 初学者来说,这个概念可能是最让人挠头的啦,毕竟在 Python 中这个特性还是挺特殊的。

在 Python 中,有两种类型的对象,可变对象和不可变对象。可变对象的状态或是内容,在运行时可以被改变,而不可变对象不可以被改变(是不是有点像绕口令)。许多自带的对象都是不可变的,包括整数 int、浮点数 float、字符串 string、布尔值 bool 以及元组 tuple 对象。

st = 'A string' 
st[0] = 'B' # 在 Python 中这样做会报错

另一方面,许多数据类型,比如列表 list、集合 set 以及字典 dict 对象是可变的,也就是你可以修改这些对象内部的元素,比如修改列表对象的第一个元素: list_[0] = 'new'

如果一个函数的默认参数是可变对象,可能会发生一些意外情况。比如下面这个函数,它的 list_ 参数的默认值是一个可变空列表

def foo(element, list_=[]):
    list_.append(element)
    return list_

让我们调用两次这个函数,而不给 list_ 参数传递任何值,这样它就会使用默认值。推想过去,这两次都应该返回一个只有单个元素的列表,因为每次调用函数的时候,list_ 应该都是取默认值为空才对。试试看:

a = foo(1) 
# 返回 [1]
b = foo(2) 
# 返回 [1,2],而不是 [2] ?这是怎么回事?

什么情况?

事实上,Python 中函数的默认参数只在函数被定义的时候进行一次求值。这意味着重复调用函数并不会重置默认参数的值,这个默认参数是会被重复使用的。

图片来源:Unsplash,摄影 Ravi Roshan

因此,如果默认参数是可变对象,它在每次函数被调用的时候都会被改变,而且这些改变的结果会影响到之后的每次调用。“标准”的做法是使用(不可变的)None 作为默认值,如下所示:

def foo(element, list_=None):
    if list_ is None:
        list_ = []
    list_.append(element)
    return list_

错误 4. 没有复制对象

复制(copy)的概念或许对初学者来说有点怪异甚至是反直觉的。举个🌰子:

你有一个列表 a = [[0,1],[2,3]],然后你声明一个新的列表,b = a,现在你有了两个内容一样的列表。

那我现在是不是就可以修改 b 而不影响 a 列表中的内容了呢?

错。

a = [[0,1],[2,3]]
b = a

b[1][1] = 100

print(a,b) 
# [[0, 1], [2, 100]] [[0, 1], [2, 100]]
print(id(a)==id(b))
# True

当你使用赋值语句来“复制”一个列表时(比如 b = a),对两个列表中任意元素的修改都会同时反映在两个对象上。赋值语句本身只是将目标对象和一个新的变量名绑定在一起,因此列表 ab 在 Python 中其实对应的是同一个引用(可以通过 id() 查看)。

该怎么复制对象呢?

如果你想要“复制”对象,单独修改其中一个的值(元素)而不影响另一个,你有两种复制的办法:浅拷贝深拷贝。让两个对象拥有不同的引用。

图片来源:Unsplash,摄影 Louis Hansel

还是用上面的例子,你可以用 b = copy.copy(a) 来创造一个 a 的浅拷贝。浅拷贝将会创造一个新的对象,里面存储的是原来那个对象里各个元素的引用。这听起来有点复杂,让我们看看实际例子:

import copy

a = [[0,1],[2,3]]
b = copy.copy(a)

print(id(a)==id(b))
# False

b[1] = 100
print(a,b)
# [[0, 1], [2, 3]] [[0, 1], 100]

b[0][0] = -999
print(a,b)
# [[-999, 1], [2, 3]] [[-999, 1], 100]
print(id(a[0]) == id(b[0]))
# True

在创造出嵌套列表 a 的浅拷贝 b 之后,两个列表对象的引用是不一样了(id(a) != id(b)),这里 != 表示“不等于”。然而,它们内部的元素还保持着相同的引用,也就是 id(a[0]) == id(b[0])

这意味着,如果修改 b 中的元素,将不会影响到 a,但如果你修改 b 中元素的元素,比如 b[1] 中的元素,则会影响到 a[1]。所以这个复制方式没有达到全部深度。

简单地说,如果 ba 的浅拷贝,对 b 中内嵌列表中的元素进行修改,也会影响到 a

如果你想要复制一个和原对象完全没有关联的对象,你需要进行深拷贝。例如,用 b = copy.deepcopy(a) 生成一个 a 的深拷贝。深拷贝将会递归地生成所有嵌套对象中的元素的拷贝。

简单地说,深拷贝对所有对象都进行复制而没有绑定。

图片来源:Unsplash,摄影 Drew Coffman

结语

好了,以上就是 Python 新人需要避免的 4 条常见错误。我用最崎岖的方式学到了教训,希望你不用重走这条弯路。

祝编码顺利!

(本文已投稿给「优达学城」。 原作: Eden Au 翻译:欧剃 转载请保留此信息)

编译来源: https://towardsdatascience.com/4-common-mistakes-python-beginners-should-avoid-89bcebd2c628

标签:UdacityTranslatePython

Powered by Jekyll on Github.io
2022 © 欧剃