python immutable对象和mutable对象

initList函数引发的问题

首先来看一个程序,这个程序的功能是创建一个长度为n,每个元素初始值为a的list

def initList(a, n, l = []):
    for i in range(n):
        l.append(a)
    return l

但是在调用该函数进行创建列表的过程中却发现该函数存在问题,比如说我想创建两个list,其中一个长度为3,初始值为4,另外一个长度为3,初始值为2,调用initList函数来创建列表。

l1 = initList(2, 3)
l2 = initList(3, 3)
print("l1: ", l1)
print("l2: ", l2)

#运行结果如下:
# l1:  [2, 2, 2, 3, 3, 3]
# l2:  [2, 2, 2, 3, 3, 3]

这个就有问题了,原本的目的是想初始化l1=[2,2,2],l2=[3,3,3]但是现在l1=l2=[2,2,2,3,3,3], 按照程序来说每次都初始化 l=[],但现在看来好像是在上一步执行的基础上执行了append函数。如果是每次都执行 l = [],那么根据python的规则,l1和l2在内存中的地址应该不同,通过使用id函数来查看l1和l2在内存中的位置。

print(id(l1))
print(id(l2))

#输出结果为:
# 634696423816
# 634696423816

通过打印在内存中的位置是一样的,这也就是说l1和l2是同一个对象,经过查阅资料发现主要是以下原因造成的:

  • only evaluates functions definitions once
  • evaluates default arguments as part of the function definition
  • allocates one mutable list for every call of that function.

也就是说在python中每个函数只编译?一次,而且默认参数会作为函数的一部分,上述的l = []是一个可变对象,究竟什么是可变对象和不可变对象?

 

python中的变量

在C语言中,如果我们新建一个变量int a =1,再执行a=2,这样修改的a所指向的内存单元的值,如果这时候再新建一个变量b,int b=a,则是将a的值拷贝到b所指向的内存里边,但是a和b其实是代表不同的内存,但是如果在python中执行类似的代码

a = 1
print("After a = 1, the address of a: ", id(a))
b = a
print("After b = a, the address of b: ", id(b))
a = 2
print("After a = 2, the address of a: ", id(a))

#执行结果
# After a = 1, the address of a:  1358218928
# After b = a, the address of b:  1358218928
# After a = 2, the address of a:  1358218960

 通过上边的结果可以看到,在执行b=a之后,b和a所指向的内存块是相同的,而在执行a=2之后,a反而指向了新的内存区域,这里和C语言有很大的不同。在python中一切皆对象,变量只是对对象的一个引用,例如执行a=1,实际上是把a指向了对象1,执行a=2之后又把a指向了对象2,1和2是不同的对象,所以地址也就不同了。

这里需要注意:python中变量对对象的引用和C++中的引用不是很一样,C++中引用和对象是绑定的,例如,定义int &b =a以后b就和a绑定了,后面不可以再把b绑定到其他对象,后边如果出现类似于b =c类似的语句,只是单纯的赋值功能,并不能重新把b引用c变量。而python中的引用和对象之间是分离的,可以随时把变量引用另外一个对象。

 

immutable对象

在python中,改变一个变量的值是经常要用到的操作,比如下面的程序

a = 1
print("After a = 1, the address of a: ", id(a))
a = a + 1
print("After a = a + 1, the address of a: ", id(a))

#执行结果
# After a = 1, the address of a:  1358218928
# After a = a + 1, the address of a:  1358218960

由结果可以看出,再执行加1之后,a的地址又变了,也就是说又指向了新的对象(原来指向的是对象1),这是因为a指向的1是一个immutable对象,也就是不可变对象,如果试图去改变一个immutable对象,在python中执行的操作就是在内存中重新创建一个对象,然后把变量指向这个新的对象,也就是说不可变对象的值是不可变的,这个类似于const类型的变量,如果试图通过某个引用去修改不可变对象,python就会在内存中新建一个对象。python中的不可变对象有以下几种:

  • int
  • float
  • decimal
  • complex
  • bool
  • string
  • tuple
  • range
  • frozenset
  • bytes

 

mutable对象

与immutable对象对应的就是mutable对象,也就说对象的值是可变的,例如我我们熟知的append函数。

l = []
l.append(1)
print("After l.append(1), the address of l: ", id(l))
l.append(2)
print("After l.append(2), the address of l: ", id(l))

#执行结果
# After l.append(1), the address of l:  586658422856
# After l.append(2), the address of l:  586658422856

 从上边执行结果可以看出,在执行完两次append之后,l的地址并没有改变,这种对象是可变的对象,也就说对象的值是可变的。对于可变对象,不用再内存中创建新的对象,直接修改原来的对象,python中的可变对象主要包括:

  • list
  • dict
  • set
  • bytearray
  • user-defined classes (unless specifically made immutable)

 

initList函数改进方案

到这里为止,明白了mutable和immutable对象的区别,再来看最开始的initList函数

def initList(a, n, l = []):
    for i in range(n):
        l.append(a)
    return l

因为python只编译函数一次,并且会把参数默认初始值也保存下来,所以实际上在编译函数的时候,l = []指向了内存中的某个位置,后边的l.append操作并不会改变这个l所指向的对象,当第二次进来这个函数的时候,l指向的还是第一次分配的那块内存的对象,这时候就有问题了,执行l.append会在原来的基础上进行运算,比如第一次执行完以后,返回l = [2,2,2],而在第二次进入函数的时候,由于l = []不再执行,所以执行l.appednd是在[2,2,2]的基础上进行的,return l也是返回的这个对象的地址,所以l1和l2本质上指向的是同一个对象,所以就产生了上面的结果:l1 = l2 = [2,2,2,3,3,3],所以在定义函数的时候,尽量不要使用mutable对象作为默认参数。通过以下方式可以继续使用该函数。

def initList(a, n, l = None):
    if l == None:
        l = []
    for i in range(n):
        l.append(a)
    return l

此时,再创建l1和l2的执行结果:

l1 = initList(2,3)
l2 = initList(3,3)
print("l1:",l1)
print("l2:",l2)

#执行结果:
# l1: [2, 2, 2]
# l2: [3, 3, 3]

 再更改过程序之后,每次进入函数l都等于None,然后执行l = [],这样每次l指向内存中不同的对象。

 

由immutable对象引发的效率问题

下面这个程序的功能是把字符串拼接在一起。

string_build = ""
for data in container:
    string_build += str(data)

 通过上面可以知道,string对象是一种immutable对象,那上面的函数每次执行string_build += str(data)这句话的时候,实际上都是先在内存中分配一个新的对象,然后把旧的对象回收掉,而且随着字符串长度的增加,后边分配的内存会越来越大,每次执行都伴随着分配和释放内存,这样做的效率是很低的。这里想减少内存分配和释放的次数,可以考虑使用mutable对象。修改程序如下

builder_list = []
for data in container:
    builder_list.append(str(data))
"".join(builder_list)

这个程序每次并不会重新创建对象,可以大大减少内存分配和释放的次数,提高程序运行效率。

总结:要弄清楚mutable和immutable的含义和工作机制,在不同的场合使用不同的方式,特别注意在作为函数默认参数的时候,尽量不要使用mutable对象。

posted @ 2018-03-31 13:38  husterxmh  阅读(459)  评论(0编辑  收藏  举报