《流畅的Python》5-对象引用,可变性,垃圾回收


很有趣的基础知识环节。

标识,相等性和别名

每个变量都有标识类型。每个对象标识只有一个,Python 里可以通过id方法去查看这个标识的整数表示,类似指针。
别名可以有很多个,引用一个对象可以有多个别名。

>>> a=[1,2,[3,4]]
>>> b=a
>>> id(a)
140322604512904
>>> id(b)
140322604512904
>>>

在 == 和 is 中选择

  • == 表示比较两个对象的值(对象保存的数据)
  • is 比较的是两个对象的标识

通常我们关注的是值而不是标识,因此==出现频率更高。
is通常用在变量和单例值之间,比如

x is None
x is not None

元组的相对不可变性

  • 元组是不可变的,但是元组里的值可变

有一个经典的例子:

>>> a=(1,2,[3,4])
>>> a[2]+=[5]
Traceback (most recent call last):
  File "<pyshell#55>", line 1, in <module>
    a[2]+=[5]
TypeError: 'tuple' object does not support item assignment
>>> a
(1, 2, [3, 4, 5])

元组的加法不是一步完成的。
这里可以深入理解一下:

  • 元组不可变,但元组内值可变
  • a[2],即[3,4]这个列表可修改,并且被修改了。
  • a[2]这个标识也不变,但是他的值变了。
  • 在最后一步给元组赋值时出现错误。

所以访问 a 的值的时候是有 5 的。

浅复制和深复制

默认做浅复制

复制列表,即用内置构造方法是浅复制。

>>> a
[1, 2, [3, 4, 5]]
>>> b=list(a)
>>> id(a)
140322604513096
>>> id(b)
140322604513480
>>> [a[i] is b[i] for i in range(len(a))]
[True, True, True]

浅复制是什么呢?

构造方法或者是 [:] ,复制了最外层容器,副本中的元素是源容器中元素的引用。如果元素都是不可变的,还可以节省内存,如果可变,就容易出错。

出错的情况有很多:

>>> a=[1,2,(3,4)]
>>> b=a[:]
>>> print(a,b)
[1, 2, (3, 4)] [1, 2, (3, 4)]
>>> a.append(100)
>>> print(a,b)
[1, 2, (3, 4), 100] [1, 2, (3, 4)]
>>> b[2]+=(5,6)
>>> print(a,b)
[1, 2, (3, 4), 100] [1, 2, (3, 4, 5, 6)]
  • 浅复制创建 a 的副本 b
  • 100 加到 a,b不会有影响
  • += 操作实际上产生了一个新的元组,所以 a 也不变。

下述操作呢?

a 仍为 [1,2,(3,4),100],b 为 [1, 2, (3, 4, 5, 6)]

>>> a[0]=-1
>>> print(a,b)
[-1, 2, (3, 4), 100] [1, 2, (3, 4, 5, 6)]
>>> a=[1,2,[3,4]]
>>> b=a[:]
>>> a[2].append(10)
>>> a
[1, 2, [3, 4, 10]]
>>> b
[1, 2, [3, 4, 10]]
  • 修改值,只变一个,这和常识是相符的
  • 对引用到相同对象的元素修改,两者都变。

注意:

以上讲到的 Python 集合都是对象的引用,而 str,bytes,array.array等 单一序列是扁平的,直接作用在内存上保存数据本身,也可以用来解释上述第一点(修改值)。

如何做深复制

直接有两个函数,copydeepcopy,深复制会去迭代循环复制对象,因此有时候太深了会遇到麻烦,根据情况可以有根据情况去实现__copy__()__deepcopy__()方法。参考copy模块。

函数的参数作为引用

要慎用可变类型作为参数的默认值。显而易见,这里不再赘述。

防御可变参数

方法很简单,以类为例,里面用self的对象替代就好了。

class TwilightBus:
    def __init__(self,passengers = None):
            if passengers is None:
                self.passengers = []
            else:
                self.passengers=passengers
    def pick(self,name):
        pass

    def drop(self,name):
        pass

垃圾回收

del语句删除名称,而不是对象。
若这个名称对应的对象在这个名称被删除后没有被引用的了,那么del会将这个对象也删除。

一种垃圾回收的算法是计数,CPython就是这么做的。显然当计数为0时,对象会被销毁。当然有其他很多垃圾回收机制,如JPython等。下面讲一下较为复杂一点的Python的回收机制。

weakref.finalize注册回调函数观察对象销毁

s1创建一个回调函数,当这个引用的对象被销毁时,调用 bye()函数。

>>> import weakref
>>> s1={1,2,3}
>>> s2=s1
>>> def bye():
    print('Gone')


>>> ender=weakref.finalize(s1,bye)
>>> ender.alive
True
>>> del s1
>>> s2='another'
Gone
>>> ender.alive
False

可以观察到,当s1del后,这个对象还在,因为被s2引用了。当s2重新引用到另一个地方的时候对象被销毁。

那么weakref.finalize是怎么知道这个对象是否还存在的呢? 可以想到用一种类似缓存的思想去实现的,这里就是弱引用

弱引用

这里介绍弱引用的存在,不讨论如何实现。

导入weakref包来看神奇的垃圾回收机制!

>>> import weakref
>>> a_set={0,1}
>>> wref=weakref.ref(a_set)
>>> wref
<weakref at 0x7f4885eab098; to 'set' at 0x7f4885e94128>
>>> wref()
{0, 1}
>>> a_set={2,3,4}
>>> wref()
{0, 1}
>>> wref() is None
False
>>> wref() is None
True
>>>

这里看得有点晕,首先要了解这是控制台会话,有一个特性是_保存上一个表达式的值。且_会自动赋值给None的对象。

wref的所指对象a_set

  • 所以a_set改变时,因为_仍然引用到{0,1},因此弱引用仍然存在。
  • 第一个wref()显然非None
  • 计算第二个wref()时,_绑定到False了,这时{0,1}不再被引用,因此wref()为空。

weakref.WeakValueDictionary简介

多用于缓存。字面意思,实例化一个字典,值为弱引用了。如果用一个普通字典去映射到实例化的WVD对象,再对普通字典进行操作,弱引用就用到了。

此外还有 weakref.WeakValueDictionary,weakref.WeakSet

最后再讲一点有趣的东西,Python对不可变类型的欺骗:
str,bytes,frozenset,tuple等等,通过copy,参数构造或[:]都不会产生副本,这是Python为了节约内存做的内部优化。

>>> s1=(1,2,3)
>>> s2=s1[:]
>>> s1 is s2
True
>>> a='123'
>>> b='123'
>>> a is b
True

称为字符串驻留

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值