写点什么

python 变量: 引用和可变性

  • 2022 年 7 月 06 日
  • 本文字数:3750 字

    阅读完需:约 12 分钟

python变量:引用和可变性

1. 再谈 python 变量:引用和可变性

学习任何一种语言都逃避不了这样一个触及灵魂的问题:值传递还是引用传递?这是一个很重要的问题,因为你有可能改变了不该改变的对象。


在 python 中,任何一个变量包括三个属性:标识,类型和值,类型很好理解,标识通常指内存中的地址,值是指变量存储的值。

1.1 变量赋值到底发生了什么?

alist = [1, 2, 3, 4]blist = [1, 2, 3, 4]aset = {1, 2, 3}adict = {'a': 1, 'b':1}atuple = ('a', [1, 2, 3])aint = 3bint = 3cint = 3555500000dint = 3555500000afloat = 4.0bfloat = 4.0
astr = 'string'bstr = 'string'
class Cla(object): def __init__(self): self.elem = [] def add(a): self.elem.append(a) def remove(a): self.elem.remove(a) acla = Cla() bcla = Cla()
复制代码


上面罗列了 python 中常见的数据类型,有列表,集合、字典,元组,数值和字符串以及类对象,其中列表,集合、字典和类对象是可变对象,元组和数值和字符串是不可变对象。


python 中可以通过id来查看变量的内存地址(Cpython 编译器中),因此可以来判定是否内存中是同一个地址变量。


我们一起来用 id 探究下变量的赋值情况


  • 变量的初始化赋值,会在内存在开辟一个空间地址存储变量,正常的情况,每个变量应该 id 都不一样,有例外吗?

  • 下面是符合预期的,两个 list(alist 和 blist)变量都是不一样,但是变量的值是一样,因此 == 是成立的。

  • 当用变量给另一个变量赋值值,传递的是引用,如 clist 和 alist 都是指向同一个内存地址,id 是一样的,可以认为 clist 是 alist 的一个别名,实质是同一个对象,因此修改 clist,alist 的内存也会跟着改变。以此类推 list,set,dict 和 tuple 都是一样


    alist = [1, 2, 3, 4]    blist = [1, 2, 3, 4]
# [1, 2, 3, 4] [1, 2, 3, 4] False True print(blist, alist, id(blist)==id(alist), blist==alist)
clist = alist # [1, 2, 3, 4] [1, 2, 3, 4] True True print(clist, alist, id(clist)==id(alist), blist==clist)
clist.append(5) # [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] True False print(clist, alist, id(clist)==id(alist), blist==clist)
# False print(id(acla) == id(bcla))
复制代码


  • 一个例外我们来看看数值和字符串有什么不一样的


    aint = 3    bint = 3    cint = 3555500000    dint = 3555500000    afloat = 4.0    bfloat = 4.0
astr = 'string' bstr = 'string'
# True True print(id(aint) == id(bint), aint==bint)
# False True print(id(cint) == id(dint), cint==dint)
# False True print(id(afloat) == id(bfloat), afloat==bfloat)
# True True print(id(astr) == id(bstr), bstr==astr)
复制代码


从上面的例子可以看出


  • 浮点型的并没有特殊之处

  • 比较小整形如 3,虽然申明了两个变量,但是地址也是一样。是不是有点奇怪,其实这个 python 为了比较常用的类型做的内存优化,python 解释器会在启动时创建出小整数池,范围是[-5,256],该范围内的小整数对象是全局解释器范围内被重复使用,永远不会被垃圾回收机制回收。

  • 比较大的整形,超出范围按照常规方式

  • 字符串也和比较小整形的方式一样的。

1.2 变量的可变性

上一小节说到 python 中列表,集合、字典和类对象是可变对象,元组和数值和字符串是不可变对象。


  • 可变对象即对象值发生变化时,地址没有发生变化

  • 不可变对象是对象值发生变化时,地址也发生了变化,相当于重新创建了一个变变量


我们拿列表 List 和元组 tuple 来举个例子:


alist = [1, 2, 3, 4]adict = {'a': 1, 'b':1}atuple = ('a', [1, 2, 3])# alist id=2680055689480, value=[1, 2, 3, 4]print('alist id={}, value={}'.format(id(alist), alist))alist.append(6)# alist id=2680055689480, value=[1, 2, 3, 4, 6]print('alist id={}, value={}'.format(id(alist), alist))

alist += [5]# alist id=2680055689480, value=[1, 2, 3, 4, 6, 5]print('alist id={}, value={}'.format(id(alist), alist))
alist = alist + [5]# alist id=2680055726664, value=[1, 2, 3, 4, 6, 5, 5]print('alist id={}, value={}'.format(id(alist), alist))
# atuple id=2680045500680, value=('a', [1, 2, 3])print('atuple id={}, value={}'.format(id(atuple), atuple))atuple += ('bd', 'c')# atuple id=2680055663416, value=('a', [1, 2, 3], 'bd', 'c')print('atuple id={}, value={}'.format(id(atuple), atuple))
复制代码


从上面的例子可以知道:


  • 列表的自我修改不会给变内存地址, 而元组地址变了

  • 需要注意的是: + 运算和 += 对于 List 的区别:+= 对于可变对象是自我修改操作,而对于不可变对象则是运算和+是一样的,这样就是重新创建一个新的变量


那么不可变对象的元素是不可以修改的吗?我们拿元组的相对不可变来说明下:


  • 元组的相对不可变性:如果元组中元素是可变, 元组中的可变元素是可以修改的


atuple = ('a', [1, 2, 3])# atuple id=2680045319240, value=('a', [1, 2, 3])print('atuple id={}, value={}'.format(id(atuple), atuple))
atuple[1].append(4)# atuple id=2680045319240, value=('a', [1, 2, 3, 4])print('atuple id={}, value={}'.format(id(atuple), atuple))
复制代码


从上可知,元组中可变对象值发生了变化,元组的引用地址没有变。

1.3 深复制和浅复制

由于变量的赋值,只是对是同一个对象标识建立一个别名,本质上是一个引用。所以当你想重新开辟一个空间时,要特别的注意;特别是多种不同对象嵌套的情况下,因为结果并不是你所预期的那样完全新的对象,可能相互影响,导致不可预见的 bug。


这也是需要了解 python 引用的原因。


copy 模块提供了对象的拷贝,通常情况是浅拷贝,对于单一对象通常是可以的。对象.copy()也可以达到一样的结果(依赖对象的具体实现)。


alist = [1, 2, 3, 4]clist = alist.copy()# alist id=2680055689288, value=[1, 2, 3, 4]# clist id=2680055728648, value=[1, 2, 3, 4]print('alist id={}, value={}'.format(id(alist), alist))print('clist id={}, value={}'.format(id(clist), clist))
alist.append(4)# [1, 2, 3, 4] [1, 2, 3, 4, 4]print(clist, alist)
复制代码


所谓浅拷贝,只拷贝第一层级的变量,但是如果是第一层级的变量是可变对象(引用对象),浅拷贝只会拷贝引用地址,这个时候改变这个可变对象,会同时影响两个父变量,这个时候需要深拷贝。


import copyalist = [1, 2, 3, 4, [1, 1]]clist = copy.copy(alist)# alist id=2680055878984, value=[1, 2, 3, 4, [1, 1]]# clist id=2680055819144, value=[1, 2, 3, 4, [1, 1]]print('alist id={}, value={}'.format(id(alist), alist))print('clist id={}, value={}'.format(id(clist), clist))
alist[-1].append(5)# alist id=2680055878984, value=[1, 2, 3, 4, [1, 1, 5]]# clist id=2680055819144, value=[1, 2, 3, 4, [1, 1, 5]]print('alist id={}, value={}'.format(id(alist), alist))print('clist id={}, value={}'.format(id(clist), clist))
复制代码


从上可知,虽然 alist 和 clist 是不同的对象,但是 alist 中可变对象值发生变化时会影响到 clist。


如果改成深度 copy 则可以解决,如下:


import copyalist = [1, 2, 3, 4, [1, 1]]clist = copy.deepcopy(alist)# alist id=2680055878984, value=[1, 2, 3, 4, [1, 1]]# clist id=2680055819144, value=[1, 2, 3, 4, [1, 1]]print('alist id={}, value={}'.format(id(alist), alist))print('clist id={}, value={}'.format(id(clist), clist))
alist[-1].append(5)# alist id=2680055878984, value=[1, 2, 3, 4, [1, 1, 5]]# clist id=2680055819144, value=[1, 2, 3, 4, [1, 1]]print('alist id={}, value={}'.format(id(alist), alist))print('clist id={}, value={}'.format(id(clist), clist))
复制代码

1.4 函数传参

python 中函数传参是共享传参,就是指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。


这种情况下,函数可能会修改作为参数传入的任何可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。


def func(a, b):    print(id(a))    a += b    print(id(a))    return a    x = 1y = 2print(x, y)print(func(x, y))print(x, y)

x = (1, 2)y = (4, 5)print(x, y)print(func(x, y))print(x, y)
x = [1, 2, 3]y = [4]print(x, y)print(func(x, y))print(x, y)
复制代码


从上面可知,对于不可变对象 +=是重新建立了新对象引用标识,不影响外部的变量的值。而对于可变对象,函数内部的修改也影响到外部的变量的值(共同的引用)


def func(a, b):    print(id(a))    a = a + b    print(id(a))    return a    x = 1y = 2print(x, y)print(func(x, y))print(x, y)

x = (1, 2)y = (4, 5)print(x, y)print(func(x, y))print(x, y)
x = [1, 2, 3]y = [4]print(x, y)print(func(x, y))print(x, y)
复制代码


而上面这个例子,因为+的运算符是构建新的变量所以都不会改变外部的值。所以需要清除你是否需要改变外部变量的值。




发布于: 刚刚阅读数: 4
用户头像

公众号:人工智能微客(weker) 2019.11.21 加入

人工智能微客(weker)长期跟踪和分享人工智能前沿技术、应用、领域知识,不定期的发布相关产品和应用,欢迎关注和转发

评论

发布
暂无评论
python变量:引用和可变性_Python_AIWeker-人工智能微客_InfoQ写作社区