@hanhan6769
2017-09-20T08:14:42.000000Z
字数 5249
阅读 780
python
那时的我经常会做同一个梦,梦里来到一个奇幻的世界,这里面似曾相识,但又觉得陌生,这应该就是童年时对XXX的向往。
——毒翼神龙
看到前面的一句话,你可能觉得这是一个写文章的,看到作者的名字,你一定觉得这个是从网络小说里面扒的,但是,其实,这个算是中国早期娱乐脱口秀视频中的一句话,视频内容讲的内容是红白机时代的水下八关,知道的基本上暴露了你的年龄。
言归正传,当年我在学python的时候也是同样的感觉,学完python基础的我觉得我已经了解了整个python语言,就像打游戏时候打通关的感觉。但是后来一次次的踩坑时候有一种很奇怪的感觉,我对python似曾相识,又觉得陌生。打游戏发现新的隐藏关卡带来的是喜悦,而python的坑带来的是自我怀疑,甚至可能是实实在在的经济损失,所以,坑,我们踩过的,一定带你绕过去。
下面让我们一起看看python隐藏的坑,排名不分先后。
基础的坑,最重要的是举一反三,最怕的是在黑板上老师写了‘一’,回家在笔记本上写一个横线就不认识这种事情。
def
,if
,while
,for
等语句第一行某位输入":"。a = list.append()
,a是不会被赋值的。dataframe.dropna()
。while True:
之后的代码块中没有跳出循环的代码。==
’号。大多数初学者,觉得‘=
’号就可以了。['a', b']
这样的错误,然后觉得自己代码没有问题的初学者。有一句歌词唱的挺好的,“跨过这道山,越过那道岭,眼前又是一座峰”。基础的坑排完了,还会有更多,更隐藏的坑等着你去踩。(比较熟悉这个歌的同学,我觉得你大概率上是北方人,更具体点的话,是东北人)
a = [1, 2, 3]
b = a
这里你认为自己构建了两个相同的列表,准备用在不同的用途。然而当开开心心的进行接下来的其他操作时,比如,对a进行修改,就发生了其他的事情。
a[1] = 100000
a
[1, 100000, 3]
b
[1, 100000, 3]
结果发现,本来希望只对a进行修改,结果发现b也受到了影响,其原因在于通过b = a的方式进行对b赋值,其实a、b指向同一个列表对象。我们可以通过查看a、b的内存地址或用is
进行验证
print(id(a), id(b))
124006856 124006856
可以看到a、b指向的其实是同一块内存
a is b
True
用is
也检测出a和b完全就是一个东西的不同名字而已。
上面的赋值方法还是比较容易看出,因为有等号,那么下面的赋值方法可能就稍微难一点看出来了。
c = [1, a, 3]
c
[1, [1, 100000, 3], 3]
当对a修改时,c同样也会受到影响
a.append(100000)
a
[1, 100000, 3, 100000]
c
[1, [1, 100000, 3, 100000], 3]
所以,不要觉得写完了,print出来的东西看着和自己想的一样就万事大吉,不实际踩一下肯定不知道接下来有坑。
那么,如何解决呢?用 .copy()
,这样就会产生两个不相干的对象,当然如果不嫌麻烦的话,可以把相同的东西再打一遍,然后,你有没有看到同行鄙视的眼神?
我们看一下效果。
a = [1, 2, 3]
b = a.copy()
a is b
False
print(id(a), id(b))
125323528 87389000
可以看到a,b指向了不同的内存地址,并且用is
检测显示是不同对象。
接下来修改a。
a[1] = 100000
a
[1, 100000, 3]
b
[1, 2, 3]
可以看到修改a已经不会对b产生影响了,此坑已填。
编程的某个时候,你希望生成这样一个嵌套列表[[],[],[],..., [],[]],
里面的列表为空或者默认值,那么第一选择肯定是利用列表乘法一次性生成多个列表,像这样
a = [[]] * 5
a
[[], [], [], [], []]
确实满足了需求,然后当你开开心心的使用时,发觉事情有点不太对。比如对a列表中作为元素的某一个列表进行修改
a[0].append(1000)
a[0]
[1000]
a
[[1000], [1000], [1000], [1000], [1000]]
然后发现,怎么所有作为元素的列表全都发生了变化。这次同样可以用id
或这is
进行检测。
print(id(a[0]), id(a[1]))
125325256 125325256
a[0] is a[1]
True
可以看出,原来a列表中作为元素的每一个列表,其实都是同一个东西,这也就解释了为什么对其中一个作为元素的列表进行原地修改时,其他所有作为元素的列表也发生了变化。
那么,解决方案如下
a = [[] for i in range(5)]
a
[[], [], [], [], []]
a[0] is a[1]
False
print(id(a[0]), id(a[1]))
125323144 125321544
可以看到,a中作为元素的列表已经不是同一个了,这样对其中的列表进行修改时候就不会影响其他列表。
a[0].append(1000)
a
[[1000], [], [], [], []]
此坑已填
刚刚了解作用域的同学应该对LEGB原则有一定了解,然后实践中可能大胆的写出了这样的函数。
a = 1
def print_a():
print(a)
a = 2
这个函数的目的也比较容易理解,打印a的值之后将a的值修改为2,但是,实际运行时发生了这样的事情。
print_a()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-24-4bb72463237f> in <module>()
----> 1 print_a()
<ipython-input-23-feb3b9a58246> in print_a()
1 a = 1
2 def print_a():
----> 3 print(a)
4 a = 2
UnboundLocalError: local variable 'a' referenced before assignment
发生这样问题的原因是在python读入并编译这段代码时,发现def里面有一个对a赋值的语句,然后决定a在函数中属于本地变量名。那么当print_a
执行时,就会对print(a)
按照LEGB原则执行搜索,发现a属于本地变量名但还未赋值。也就是我们在前面基础坑里面提到的在变量未赋值前进行使用。
解决方案需要使用global
声明a是一个全局变量。
a = 1
def print_a():
global a
print(a)
a = 2
print_a()
1
a
2
可以看到,函数已经可以正常使用,并且全局变量a按照预期进行了修改。此坑已填。
默认参数在def语句运行时完成了保存,对于可变对象而言,当函数被调用并对该参数进行原地修改,默认参数将发生变化并进而影响接下来的函数调用。
先用可变对象作为默认参数编写一个函数。
def test(a=[]):
a.append(1)
print(a)
接下来多次调用test
函数,你一定以为每次打出来的都是一个包含1的列表。但是,事实并不是这样。
test()
[1]
test()
[1, 1]
test()
[1, 1, 1]
是不是感觉三观尽毁啊。
当然,有坑就会有填坑的方案。官方给出的标准解决方案是这样的。
def test(a=None):
if a is None:
a = []
a.append(1)
print(a)
test()
[1]
test()
[1]
可以看到,test
多次被调用已经不会出现之前的情况了。此坑已填。
在知道python中一切皆对象之后,也许你就会尝试把一些特别的东西放进一个list,虽然不清楚未来实际会不会用到这样的写法,反正先试试。然后你就想将一套简单的函数放进一个列表,然后用lambda
编写。
func_list = []
for i in range(5):
func_list.append(lambda x: x + i)
这样生成了一个元素为函数的列表,其中每个函数都可以传入一个参数,然后返回的是传入值和0,1,2,3,4的和。当然理想情况下是这样,现实并不会如此。
func_list[0](1)
5
func_list[1](1)
5
func_list[2](1)
5
可以发现,所有结果都是4+1的和。发生这种现象的原因在于变量i
在函数调用时才进行查找,而此时循环已经结束,i=4
,所以所有函数对象在被调用时参数i
全部都为5.
那么解决方案就是按照本文2.4中写的,默认参数在def
语句运行时完成了保存,也就是使用默认参数的方式解决该问题。
func_list = []
for i in range(5):
func_list.append(lambda x, i=i: x + i)
func_list[0](1)
1
func_list[1](1)
2
func_list[2](1)
3
可以看到,问题已经得到解决。此坑已填。
熟悉列表,学习python算是入门;熟悉enumerate,学习python可以说不是纯小白了。enumerate返回下标和元素的方式还是为python使用者提供了不大不小的便利,不过同时也挖下了一个坑。
比如你写了一个循环,想从列表中删除偶数。
a = [1,2,3,4,5,6]
for i, item in enumerate(a):
if a[i] % 2==0:
del a[i]
a
[1, 3, 5]
是不是想说没什么错误啊,然后在给你看看这个。
a = [1,2,6,3,4,4]
for i, item in enumerate(a):
if a[i] % 2==0:
del a[i]
a
[1, 6, 3, 4]
enumerate(a)
<enumerate at 0x78e6d80>
产生这个问题的原因是在当i=1
时,a[1]
的值为2,符合条件,那么删除2这个值之后,a整个列表发生了变化,6这个数值前进一个位置到了a[1]的位置,然后i在执行下一次循环时候值取了2,我们以为应该对应着检验6,其实6被前移一个位置,也就被遗漏了。
下面我们将for 循环的部分手动执行,看现实是不是和我们理解的一样。
a = a = [1,2,6,3,4,4]
e = enumerate(a)
e
<enumerate at 0x78edc18>
i, item = e.__next__()
(i, item)
(0, 1)
if a[i] % 2==0:
del a[i]
a
[1, 2, 6, 3, 4, 4]
此时我们可以看到,执行完循环的第一步,a并没有发生变化。接下来我们继续第二步。
i, item = e.__next__()
(i, item)
(1, 2)
if a[i] % 2==0:
del a[i]
a
[1, 6, 3, 4, 4]
可以发现,a已经发生了变化,那么我们接下来只要看在enumerate提供新的下标好元素是不是还按照未调整的a进行提供的。当然,可以告诉你们答案,肯定不是了。
i, item = e.__next__()
(i, item)
(2, 3)
可以看到6也通过向前移动一个位置的方式逃过了检查。这个坑真的是非常难发现,因为有时候碰巧你的运算方式和提供的列表刚刚好结果是你想要的,然后让你觉得这样用就是正确的。这种时候就非常可怕了。
这个坑的解决方案可以使用列表解析式添加筛选条件即可。
a = [1, 2, 6, 3, 4, 4]
a = [x for x in a if x%2 != 0]
a
[1, 3]
此坑已填。
看这里。
不用怀疑你的眼睛和设备,上面就是空白的。
如果我在这儿就把高阶篇写完了,这个系列我还能写啥别的?
写到这里,python编程一些比较基础的坑也已经描述的比较详细了。我从来不产生失落感,我只是信心的粉碎机。当你觉得自己python已经学的差不多的时候,总会有那么一个人、或者一篇文章告诉你,你懂得还不够多。
不过,坑总是有的,学习的时候你可以抱着填坑的心态,也可以怀着获取的目的,两者都取决于你的选择。而且,个人觉的,怀着获取的态度,容易满足,不断追求,也就一直快乐。
最后,还是用毒翼神龙的一段话结个尾:
“几盘卡带就可以陪我们走过整个童年的那个时代恐怕也一去不复返了吧
每天的生活被工作、学习、应酬充斥着
也让我们渐渐忘记了那个传说
虽然它是一段美丽的谣言
但想起那个信以为真的年纪,仍然有许多美好的回忆
我怀念那时单纯的快乐
怀念那个很容易就能满足的童心”
(修改一句)
“现在,这些美好的东西还可以存在
我们可以做的
还有保持一颗向前走的心
多学python”