python坑爹的机制

130 阅读6分钟

「这是我参与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
[]
"""