Skip to content

Latest commit

 

History

History
1008 lines (479 loc) · 19.9 KB

变量引用与垃圾回收.md

File metadata and controls

1008 lines (479 loc) · 19.9 KB

变量引用与垃圾回收

中文中太阳叫"太阳",英语中太阳叫"sun",法语中太阳叫"soleil",日语中太阳叫"たいよう",不同的叫法其实指的是同一个东西.

python中的变量就有点像各种语言中的名词,它只是代表一个对象而已.

通常我们将变量与对象的关系比作打标签,变量就是我们的标签,而对象就是要被打标签的东西,而这一过程就被称为赋值

python中的变量与对象的关系比较类似java中的引用,或者说是C中的指针变量.

变量声明和赋值

在前面的例子中我们就可以看出赋值操作使用=,=的左边是变量,右边是值对象,python解释器中没有专门的变量声明语法,变量的第一次赋值就相当于是声明.因此如果我们在条件块中赋值变量,在外部再次使用这个变量就会因为没有进入分支而报错.

赋值操作=并不是表达式,因此它并没有值,在python中有一个特殊的赋值表达式操作:=(海象运算符),其语法为(variable_name := expression or value)它的值就是被赋值的变量的值

(x := 1)
1

赋值表达式的作用主要是可以缓存右侧表达式中的值到一个变量中,因此可以在一些场景下减少重复代码和重复计算,用的最多的就是序列推导,我们要解析如下一个字符串,将它解析为一个字典

x = "1:2,3:4,5:6"

传统方法会这样写

{i.split(":")[0]:i.split(":")[1] for i in x.split(",") if len(i.split(":")) ==2}
{'1': '2', '3': '4', '5': '6'}

这样写的问题在于我们每次执行会对拆分出来的i执行3次split操作,既啰嗦又浪费算力,因此在不使用赋值表达式的情况下并不推荐使用序列解析的方式实现这个需求,for循环可能是更棒的选择;但如果使用赋值表达式,那么就可以完美解决这个问题.

{j[0]:j[1] for i in x.split(",") if len(j := i.split(":")) ==2}
{'1': '2', '3': '4', '5': '6'}

标识,相等性和别名

我们还是用之前的扑克牌来做例子

from collections import namedtuple
Card = namedtuple('扑克牌', ['大小', '花色'])
红桃A = Card("A","红桃")
红桃A
扑克牌(大小='A', 花色='红桃')
红桃Ace = 红桃A
红桃A is 红桃Ace
True

可以看到,红桃A和红桃Ace其实是同一个东西.这边又有了一个新的问题,怎么看出来这两个变量其实是一个对象呢?

id(红桃A),id(红桃Ace)
(4470352992, 4470352992)

内置方法id()可以检查对象identity,每个对象在生成的时候就会产生一个identity,同一个进程中同一时间不会存在不同的identity在虚拟机中,cpython中对象的identity是其内存中的空间.

is运算符专门用来判别变量指向的对象的identity是否一样.也就是是不是指向同一个对象.

is和==

python中也常会有要判别两个对象是否相等的情况

import copy
def one():
    return [1,2,3]
a = one()
b = copy.copy(a)
a == b
True
a is b
False

a和b不是同一个对象,但内容一样.那为啥可以用==判断呢?==实际上是调用对象的魔术方法__eq__而的运算来的,只要在对象中改写这个方法其实也可以让a不等于b,不过__eq__是无法在外部改写的,这也相对增加了安全性

a.__eq__(b)
True
a.__eq__ = lambda x:False
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

<ipython-input-14-39fe0e3412b0> in <module>()
----> 1 a.__eq__ = lambda x:False


AttributeError: 'list' object attribute '__eq__' is read-only

可变对象与不可变对象

一般来说python中的对象分为两类

  1. 不可变对象
  2. 可变对象

不可变对象包括str,bytes和数字类型,他们特点就是存在内存中,对象的内容是不可变的.

可变对象包括list,dict,set,以及自定义类型的实例等.

对象复制

python标准库提供了一个用于复制可变对象的工具copy

import copy
a = [1,2,3,4,5]
id(a)
4468230152
aa = copy.copy(a)
id(aa)
4468229960
a == aa
True
a is aa
False

像python内置的容器,直接使用自身作为参数实例化一个新对象可以简单的复制

ab = list(a)
id(ab)
4468181832

list有一个语法糖,可以简单的复制原有列表

aaa = a[:]
id(aaa)
4468179848

浅复制和深复制

浅复制是指复制了最外层容器,副本中的元素是源容器中元素的引用.而深复制则是完全复制.python默认使用浅复制.

对于浅复制,如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题.

python中浅复制和深复制可以分别使用copy.copy(object)copy.deepcopy(object)来实现,而对象复制操作对应的接口为__copy__()__deepcopy__()

下面一个例子(来自流畅的python例8-8)可以用来对比浅复制和深复制的差别

class Bus:
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)
    def pick(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)
import copy
bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
id(bus1), id(bus2), id(bus3)
(4471042848, 4471042792, 4471043128)
bus1.drop('Bill')
bus1.passengers
['Alice', 'Claire', 'David']
bus2.passengers
['Alice', 'Claire', 'David']
id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4468181640, 4468181640, 4468181320)
bus3.passengers
['Alice', 'Bill', 'Claire', 'David']

bus1 和 bus2 共享同一个列表对象,因为 bus2 是 bus1 的浅复制副本。

函数参数作为引用

Python唯一支持的参数传递模式是共享传参(call by sharing).多数面向对象语言都采用这一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用类型是这样,基本类型按值传参).共享传参指函数的各个形式参数获得实参中各个引用的副本.也就是说,函数内部的形参是实参的别名.

这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象).下例中有个简单的函数,它在参数上调用+=运算符.分别把数字、列表和元组传给那个函数,实际传入的实参会以不同的方式受到影响.

def f(a, b):
    a += b
    return a
x = 1
y = 2
f(x, y)
3
a = [1, 2]
b = [3, 4]
f(a, b)
[1, 2, 3, 4]
a, b
([1, 2, 3, 4], [3, 4])
t = (10, 20)
u = (30, 40)
f(t, u)
(10, 20, 30, 40)
t, u
((10, 20), (30, 40))

不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是Python函数定义的一个很棒的特性,这样我们的API在进化的同时能保证向后兼容。然而,我们应该避免使用可变的对象作为参数的默认值.

下面的例子中我们用之前的Bus类为基础定义一个新类,HauntedBus,然后修改 __init__ 方法。这一次,passengers的默认值不是None,而是[], 这样就不用像之前那样使用if判断了.这个'聪明的举动'会让我们陷入麻烦.

class HauntedBus: 
    """备受幽灵乘客折磨的校车"""
    def __init__(self, passengers=[]):
        self.passengers = passengers 
    def pick(self, name): 
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)

然后就会出现下面的诡异行为

bus1 = HauntedBus(['Alice', 'Bill'])
bus1.passengers
['Alice', 'Bill']
bus1.pick('Charlie')
bus1.drop('Alice')
bus1.passengers 
['Bill', 'Charlie']
bus2 = HauntedBus()
bus2.pick('Carrie')
bus2.passengers
['Carrie']
bus3 = HauntedBus()
bus3.passengers
['Carrie']
bus3.pick('Dave')
bus2.passengers
['Carrie', 'Dave']
bus2.passengers is bus3.passengers
True
bus1.passengers
['Bill', 'Charlie']

问题就在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表.

这种问题很难发现.如示上例所示,实例化HauntedBus时,如果传入乘客,会按预期运作.但是不为HauntedBus指定乘客的话,奇怪的事就发生了,这是因为self. passengers变成了passengers参数默认值的别名.

出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性.因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响.

HauntedBus.__init__.__defaults__
(['Carrie', 'Dave'],)

我们可以验证bus2.passengers是一个别名,它绑定到HauntedBus.__init__.__ defaults__ 属性的第一个元素上

可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值

防范可变参数

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数.

例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部?

具体情况具体分析。这其实需要函数的编写者和调用方达成共识.

最后一个校车示例中,TwilightBus实例与客户共享乘客列表,这会产生意料之外的结果.在分析实现之前,我们先从客户的角度看看TwilightBus类是如何工作的.

class TwilightBus:
    """让乘客销声匿迹的校车"""
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = passengers 
    def pick(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] 
bus = TwilightBus(basketball_team)
bus.drop('Tina') 
bus.drop('Pat')
basketball_team 
['Sue', 'Maya', 'Diana']

TwilightBus 违反了设计接口的最佳实践,即'最少惊讶原则'.学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶.

除非本来就有这种需求,否则我们应该让校车自己维护乘客列表

class Bus:
    """行为正常的校车"""
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = passengers[:] 
    def pick(self, name):
        self.passengers.append(name)
    def drop(self, name):
        self.passengers.remove(name)
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] 
bus = Bus(basketball_team)
bus.drop('Tina') 
bus.drop('Pat')
basketball_team 
['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus.passengers
['Sue', 'Maya', 'Diana']

内置方法del和垃圾回收

对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收.

del语句

del语句删除名称,而不是对象.del命令可能会导致对象被当作垃圾回收,但是那仅是当删除的变量保存的是对象的最后一个引用,或者无法得到对象时的情况.重新绑定名字到其他对象如None也可能会导致对象的引用数量归零,导致对象被销毁.

有个__del__特殊方法,但是它不会销毁实例而是在即将销毁实例之前触发,它不应该在代码中调用.

即将销毁实例时,Python解释器会调用__del__方法,给实例最后的机会,释放外部资源.自己编写的代码很少需要实现 __del__代码,有些python 新手会花时间实现,但却吃力不讨好,因为__del__很难用对.具体的可以看[Python 语言参 考手册中'Data Model'一章中__del__特殊方法的文档](https://docs.python. org/3/reference/datamodel.html#object.del)

垃圾回收

在 CPython 中,垃圾回收使用的主要算法是引用计数.实际上,每个对象都会统计有多少引用指向自己.当引用计数归零时,对象立即就被销毁:

  1. CPython 会在对象上调用__del__方法(如果定义了),
  2. 然后释放分配给对象的内存

CPython 2.0 增加了分代垃圾回收算法, 用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使再出色的引用方式也会导致组中的对象不可获取. Python的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象的引用数量为零时可能不会立即调用__del__方法.

为了演示对象生命结束时的情形,下例使用weakref.finalize注册一个回调函数,在销毁对象时调用.

import weakref
s1 = {1, 2, 3}
s2 = s1
bye = lambda :print('对象随风而逝~')
ender = weakref.finalize(s1, bye)
ender.alive
True
del s1
ender.alive
True
s2 = 'spam'
对象随风而逝~
ender.alive
False

你可能觉得奇怪,为什么上例中的{1, 2, 3}对象被销毁了?毕竟,我们把s1引用传给finalize函数了,而为了监控对象和调用回调,必须要有引用。这是因为,finalize持有{1, 2, 3}的弱引用

弱引用

正是因为有引用,对象才会在内存中存在.当对象的引用数量归零后,垃圾回收程序会把对象销毁.但是,有时需要引用对象,而不让对象存在的时间超过所需时间.这经常用在缓存中.

弱引用不会增加对象的引用数量.引用的目标对象称为所指对象(referent).因此我们说,弱引用不会妨碍所指对象被当作垃圾回收.

弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象.

下例展示了如何使用weakref.ref实例获取所指对象.如果对象存在,调用弱引用可以获取对象;否则返回None.

weakref.getweakrefcount(object) 可以获取对象object关联的弱引用对象数

weakref.getweakrefs(object)可以获取object关联的弱引用对象列表

import weakref
def callback(reference):  
    """Invoked when referenced object is deleted"""  
    print('callback(', reference, ')') 
obj = {2, 3} 
r = weakref.ref(obj, callback)  
  
print('obj:', obj)
print('ref:', r)
print('r():', r())
print('deleting obj')
del obj  
print('r():', r())
obj: {2, 3}
ref: <weakref at 0x10a7f9c78; to 'set' at 0x109105ac8>
r(): {2, 3}
deleting obj
callback( <weakref at 0x10a7f9c78; dead> )
r(): None

代理Proxy

使用weakref.proxy和使用普通weakref的区别就是不需要(),可以像原对象一样地使用proxy访问原对象的属性.

import weakref  
 
def test_func(reference):  
  
    print('Hello from Callback function!') 
  
     
a = {1,2,3}
  
#建立一个对a的代理(弱引用)  
  
x = weakref.proxy(a, test_func)  
  
print(a)
  
print(x) 
  
del a
{1, 2, 3}
{1, 2, 3}
Hello from Callback function!

weakref模块的文档指出,weakref.ref类其实是低层接口,供高级用途使用,多数程序最好使用weakref集合finalize.也就是说,应该使用WeakKeyDictionaryWeakValueDictionaryWeakSetfinalize(在内部使用弱引用),不要自己动手创建并处理weakref.ref实例.

下面以WeakValueDictionary为例子看看weakref的高级接口如何使用.

WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用.被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除.因此,WeakValueDictionary经常用于缓存.

我们对WeakValueDictionary的演示是奶酪店,客户问了40多种奶酪,包括切达干酪和马苏里拉奶酪,但是都没有货.

class Cheese:
    def __init__(self, kind):
              self.kind = kind
    def __repr__(self):
        return 'Cheese({self.kind})'.format(self=self)

我们把catalog中的各种奶酪载入WeakValueDictionary实现的stock中.然而,删除catalog后,stock 中只剩下一种奶酪了.

import weakref
stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]
for cheese in catalog:
    stock[cheese.kind] = cheese 
sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']
del catalog
sorted(stock.keys())
['Parmesan']
del cheese
sorted(stock.keys())
[]

删除catalog之后,stock中的大多数奶酪都不见了,这是WeakValueDictionary的预期行为.为什么不是全部呢?

临时变量引用了对象,这可能会导致该变量的存在时间比预期长.通常,这对 局部变量来说不是问题,因为它们在函数返回时会被销毁.但是在上例中,for循环中的变量cheese是全局变量,除非显式删除,否则不会消失

WeakValueDictionary对应的是 WeakKeyDictionary,后者的键是弱引用.

WeakKeyDictionary实例可以为应用中其他部分拥有的对象附加数据,这样就无需为对象添加属性.这对覆盖属性访问权限的对象尤其有用.

weakref模块还提供了WeakSet类,按照文档的说明,这个类的作用很简单--'保存元素弱 引用的集合类.元素没有强引用时,集合会把它删除'

如果一个类需要知道所有实例,一种好的方案是创建一个WeakSet类型的类属性,保存实例的引用.如果使用常规的set,实例永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与Python进程一样 长,除非显式删除类.

这些集合,以及一般的弱引用,能处理的对象类型有限.不是每个Python对象都可以作为弱引用的目标(或称所指对象).基本的 listdict实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题

class MyList(list): 
    """list的子类,实例可以作为弱引用的目标"""
    pass
a_list = MyList(range(10))
# a_list可以作为弱引用的目标 
wref_to_a_list = weakref.ref(a_list)

set实例可以作为所指对象,因此上例才使用set实例.用户定义的类型也没问题,但是inttuple实例不能作为弱引用的目标,甚至它们的子类也不行。 这些局限基本上是CPython的实现细节,在其他Python解释器中情况可能不一样.这些局限是内部优化导致的结果.

Python对不可变类型施加的把戏

python的内部有一种优化措施较驻留(interning).他的结果之一就是共享字符串字面量,以及在小的整数防止重复创建'热门'数字,如 0、—1 和 42。注意,CPython不会驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明.这一优化措施可以节省内存,提升解释器的速度.但只有不可变类型会受到影响.这也是为什么弱引用在int,tuple这类不可变类型中无法使用的原因