「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
因为参与了更文挑战。导致最近不得不去看一下技术博客,来提升(水)思(内)路(容)。
然后正巧最近又被python各种折磨,就发现了大佬对python的坑进行侦查,这里我对代码就进行了复现。
坑1 可变对象做默认参数
这个坑点可能不去研究,根本不会注意到,一旦忽略这个问题,如果该函数存在频繁调用,多半有可能内存泄漏,自己排查都排查不出来。
def f1(l=[]):
l.append(1)
return l
print(f1())
# [1]
print(f1())
# [1,1]
究其原因,第一是python的机制是万物皆是对象,函数也是对象,默认参数相当于类属性,在定义的时候就已经求值了。第二,python list作为全局变量是可以不声明的,导致上面这种情况。
对一点做一个补充实验,这个实验就很具说明性
import datetime
import time
print(datetime.datetime.now())
# 可变参数
def f2(now=datetime.datetime.now()):
print(now)
print("默认参数定义直接会求值")
f2()
time.sleep(10)
f2()
# 解决方法
def f2(now=None):
if not now:
now=datetime.datetime.now()
print(now)
print("解决方法")
f2()
time.sleep(10)
f2()
输出结果
2022-02-22 09:59:40.924305
默认参数定义直接会求值
2022-02-22 09:59:40.924305
2022-02-22 09:59:40.924305
解决方法
2022-02-22 09:59:50.938424
2022-02-22 10:00:00.948943
建议
避免在默认参数中使用可变参数。
坑2 x = x + y 不等价于 x += y
x = 1
y = 2
x = x + y
# x = 3
x += y
# x = 3
x = [1]
y = [2]
x = x + y
# x = [1,2]
x += y
# x = [1,2]
目前从结果上看是没有问题的。补充实验就可以看出差距。
x = [1]
y = [2]
print(id(x))
# 1724089425536
x = x + y
print(id(x))
# 1724089424960
x = [1]
y = [2]
print(id(x))
# 2726447996544
x += y
print(id(x))
# 2726447996544
x=x+y 是重新分配一个空间
x+=y 是在原来的空间上修改
坑3 元组和逗号
元组单一元素要加逗号,不加逗号就表示这个元素本身,这个应该是默认潜规则了。这是和python设计的机制有关,设计元组是不可变,据说是避免歧义和运算的括号混淆,这里不去纠结设计语言的程序员怎么想的,这里知道这个结论就行。
t=(1,2)
print(type(t))
# <class 'tuple'>
t=(1)
print(type(t))
# <class 'int'>
t=(1,)
print(type(t))
# <class 'tuple'>
坑4 生成一个元素是列表的列表
当然生成一个元素是字典的列表也是可以的,更通俗的说,生成一个元素是可变对象的序列
a= [[]] * 10
print(a)
# [[], [], [], [], [], [], [], [], [], []]
a[0].append(10)
print(a[0])
# [10]
print(a[1])
# [10]
print(a)
# [[10], [10], [10], [10], [10], [10], [10], [10], [10], [10]]
造成上述结果的原因是a[]所以列表都指向同一个,使用字典也是同理
a= [[]] * 10
print(id(a[0]))
# 2340927495808
print(id(a[1]))
# 2340927495808
print(id(a[5]))
# 2340927495808
print(id(a[9]))
# 2340927495808
a= [{}] * 10
print(id(a[0]))
# 1627232925696
print(id(a[1]))
# 1627232925696
print(id(a[5]))
# 1627232925696
print(id(a[9]))
# 1627232925696
建议
需要生成二维数值,或者元素是字典的列表
a = [[] for _ in range(10)]
print(a)
# [[], [], [], [], [], [], [], [], [], []]
a[0].append(10)
print(a[0])
# [10]
print(a)
# [[10], [], [], [], [], [], [], [], [], []]
print(id(a[0]))
# 2482624883392
print(id(a[5]))
# 2482625046080
print(id(a[9]))
# 2482625046528
坑5 遍历列表是对列表元素进行修改
这里的逻辑就很简单,传入l2,在遍历到i=1时,发现l2[i]符合删除条件,删除之后,l2=[1,4,6,3,5],列表变短了,在遍历到i=2时l2[2]不是4而是6。
def f6(l):
for i,e in enumerate(l):
if e %2 ==0:
del l[i]
l=[1,2,3,4,5,6]
f6(l)
print(l)
# [1,3,5]
l2=[1,2,4,6,3,5]
f6(l2)
print(l2)
# [1, 4, 3, 5]
建议
谨慎在列表循环中删除元素,最好使用python列表推导式
l2=[1,2,4,6,3,5]
l2=[i for i in l2 if i%2!=0]
print(l2)
# [1, 3, 5]
坑6 闭包和lambda匿名函数
预期结果应该是0,2,4,6,8完全预期结果不符
由于出现这个陷阱的时候经常使用了lambda,所以可能会认为是lambda的问题,但lambda表示不愿意背这个锅。问题的本质在与python中的属性查找规则,LEGB(local,enclousing,global,bulitin),在上面的例子中,i就是在闭包作用域(enclousing),而Python的闭包是 迟绑定 , 这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。
简单一点来说就是,当x=2通过lambda闭包传入时,i已经完成了循环,这个时候的i变成了4
def f6():
return [lambda x:i*x for i in range(5)]
for m in f6():
print(m(2))
"""
8
8
8
8
8
"""
建议
def f6():
return [lambda x,i=i:i*x for i in range(5)]
for m in f6():
print(m(2))
"""
0
2
4
6
8
"""
坑7 内存泄露
python引用计数 + 分代收集和标记清除(处理循环引用),进行垃圾回收,但如下两种情况依旧存在内存泄露:
- 第一是对象被另一个生命周期特别长(如全局变量)的对象所引用
- 第二是循环引用中的对象定义了
__del__函数,简而言之,循环引用中Python无法判断析构对象的顺序,无法释放
不过python3.4已经可以自动处理带有__del__方法的循环引用,也不会发生内存泄露了
reachable是针对python对象而言,如果从根集(root)能到找到对象,那么这个对象就是reachable,与之相反就是unreachable,unreachable只存在于循环引用中的对象,Python的gc模块就是针对unreachable对
collectable是针对unreachable对象而言,如果这种对象能被回收,是collectable,如果不能被回收,即循环引用中的对象定义了__del__, 那么就是uncollectable。 即unreachable (循环引用)分成 collectable和ubcollectable(del)
gc.garbage: 返回是unreachable对象,且不能被回收的的对象,如果设置SAVEALL,所有unreachable都加入此列表
import gc
class Foo(object):
def __init__(self):
self.bar = None
print('foo init')
def __del__(self):
print("foo del")
class Bar(object):
def __init__(self):
self.foo = None
print('bar init')
def __del__(self):
print('bar del')
def collect_and_show_garbage():
print("Collecting...")
n = gc.collect()
print("unreachable objects:", n)
print(gc.garbage)
def func():
foo = Foo()
bar = Bar()
foo.bar = bar
bar.foo = foo
# gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_UNCOLLECTABLE)
func()
collect_and_show_garbage()
"""
foo init
bar init
Collecting...
foo del
bar del
unreachable objects: 4
[]
"""