对象引用-可变性-垃圾回收

变量

python和java中的变量本质不一样, python中的变量本质是指针, 类似生活中的便利贴, 可以贴在任何对象上。(java中的变量像一个已经定制好的盒子)。

a = 1  

过程 :

  1. 先到内存中申请一块int空间
  2. 把 a 贴在这块内存空间上

便利贴的大小是固定的, 所以a可以帖在任何对象上

a = 'abc'

两个指针指向同一个对象(两个便利贴贴在同一个物体上)

a = [1, 2, 3]
b = a
b.append(4)
print(a)	# [1, 2, 3, 4]
print(a is b)   # True

在这里插入图片描述

python赋值方式:”,而不是“把 对象 分配给变量 ”。对 <引用式变量> 来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。

为了理解 Python 中的赋值语句,应该始终先读右边。对象在 右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像 为对象贴上标注.

== 与 is 的区别

  • is 是根据id值判断是否是同一个对象
  • ==符号是调用魔法函数__eq__, 判断值是否是相等的
a = [1, 2, 3]
b = [1, 2, 3]
print(a is b)	# False
print(a == b)	# True

注意: intern机制

a = 1
b = 1
print(a is b) 	# True

intern机制: 一定范围内的小整数, 会建立全局唯一的对象

下次再用这个小整数时会直接指向之前建立的小整数, 对于小段的字符串也服从intern机制.

判断 x是不是None : x is None

否定的正确写法: x is not None 符合英文的语法, 而不是 x not is None

is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用 特殊方法,而是直接比较两个整数 ID。
而 a == b 是语法糖,等同于 a.__eq__(b)。继承自 object 的 __eq__ 方法比较两个对象的 ID,结 果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 __eq__ 方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例 如,比较大型集合或嵌套层级深的结构时

del语句和垃圾回收机制

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

del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。 重新绑定也可能会导致对象的引用数量归零,导致对象被 销毁。 python中的垃圾回收的算法采用的是 引用计数.
a = 1
b = a

a上会有一个计数器

a = 1 时在计数器上加一

b指向1, 在计数器上在加一

del a

del a 后将计数器减一

当计数器减到零时, python解释器才会将对象回收

a = object()
b = a
del a
print(b)	# <object object at 0xxxxxx> 虽然删除了变量a, 但变量b依旧指向对象, 即, del删除的只是名称
print(a)	# 报错, NameError, 'a' is not defind
如果两个对象相互引用,当它们的引用只存在二者之间时,垃圾回收程序 会判定它们都无法获取,进而把它们都销毁。
#循环引用:b 引用 a,然后追加到 a 中
a = [1, 2]
b = [a, 3]
a.append(b)  # 此时, a, b两个对象相互引用
print(a)  # [1, 2, [[...], 3]]
print(b)  # [[1, 2, [...]], 3]
del a
print(a)  # TypeError: Name 'a' is not defind
print(b)  # [[1, 2, [...]], 3]     摘自<流畅的python>, 难道有错?

为了演示对象生命结束时的情形,注册一个回调函数,在销毁对象时调用。

import weakref

s1 = {
    
    1, 2, 3}
# 如果s1是列表, 会在第9行报错TypeError: cannot create weak reference to 'list' object
s2 = s1  # s1, s2都是别名, 指向了对象{1, 2}
# 这个函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用
def bye():	
    print('gone with wind')
ender = weakref.finalize(s1, bye)
print(ender.alive)  # Ture
del s1
print(ender.alive)  # True
del s2    # 'gone with wind'
print(ender.alive)  # False
你可能觉得奇怪,为什么示例中的 {1, 2, 3} 对象被销毁了? 毕竟,我们把 s1 引用传给 finalize 函数了,为了监控对象和调用回调,必须要有引用。这是因为,finalize持有 {1, 2, 3} 的 .

弱引用

弱引用不会增加对象的引用数量
正是因为有引用,对象才会在内存中存在

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

例: 弱引用是可调用的对象,返回的是被引用的对象;如果 所指对象不存在了,返回 None

>>> import weakref 
>>> a_set = {
    
    0, 1} 
>>> wref = weakref.ref(a_set)>>> wref 
<weakref at 0x100637598; to 'set' at 0x100636748> 
>>> wref(){
    
    0, 1} 
>>> a_set = {
    
    2, 3, 4}>>> wref(){
    
    0, 1} 
>>> wref() is NoneFalse 
>>> wref() is NoneTrue

WeakValueDictionary介绍

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

因 此,WeakValueDictionary 经常用于缓存。

import weakref
class Cheese:
    def __init__(self, kind):    self.kind = kind

    def __repr__(self):    return 'Cheese(%r)' % self.kind

stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')] # 注意这里最后一个添加的是Parmesan
for cheese in catalog:
    stock[cheese.kind] = cheese

print(sorted(stock.keys()))  # ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']
del catalog
print(sorted(stock.keys()))  # ['Parmesan']  还剩下一个
del cheese  # 提示cheese is  not defined 但是运行没有错误 !
print(sorted(stock.keys()))  # []
#删除 catalog 之后,stock 中的大多数奶酪都不见了,
#这是 WeakValueDictionary 的预期行为。为什么不是全部呢

注意: 在上面的代码中, cheese是一个全局遍历. 它依旧指向着最后一个添加的对象, 如果是局部变量则不受影响.

一个典型的参数错误

当默认参数是一个可变对象时
定义默认参数时要牢记一点: 默认参数必须指向不可变对象!
class Company:
    def __init__(self, com_name, staffs=[]):	# 如果默认参数是一个列表
        self.com_name = com_name
        self.staffs = staffs
    def add(self, name):
        self.staffs.append(name)
    def remove(self, name):
        self.staffs.remove(name)

if __name__ == '__main__':
    com1 = Company('com1')	# 注意此时没有传递staffs列表
    com1.add('Mike')
    com2 = Company('com2')  # 创建第二个对象时也没有传递staffs列表
    com2.add('Bob')
    print(com1.staffs)		# !!! 输出: ['Mike', 'Bob']
    print(com2.staffs)		# !!! 输出: ['Mike', 'Bob']

为什么两个不同对象的列表输出相同的内容?

原因:

  1. 默认参数是一个可变对象
  2. 在传参时都没有对其传递参数, 所以都使用了一个默认的列表, 对象共享
print(com1.staffs is com2.staffs)  	# True
# 实际上这个默认值可以直接通过类名.__init__.__defaults__来获取
print(Company.__init__.__defaults__)	# ['Mike', 'Bob']
# 为了避免这个问题, 应该把传入的默认参数设置为不可变
def __init__(self, c_name, staffs=None):	# None
    if staffs is None:
        self.staffs = []
    else:
        self.staffs = list(staffs)  # 注意这里要创建列表的副本, 否则, 会影响到源列表 !
    self.c_name = c_name

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

元组的相对不可变性

元组保存的是对象的引用
元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是 也就是说,`元组的不可变性`其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

而 str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)

元组的值会随着引用的可变对象的变化而变。元组中不 可变的是元素的标识。 ```python t1 = (0, 1, [2, 3]) print(id(t1[-1])) # 查看元组最后一个元素的标识 t1[-1].append(4) print(id(t1[-1])) # 两次标识不变 ``` 元组的 解释了有些元组不可散列 的原因。
l1 = [[1, 2], (3, 4)]
l2 = list(l1)   # 这是使用 构造方法, 等同 [:]
# 浅拷贝
l1 is l2  # Flase
print(l1[0] is l2[0], l1[1] is l2[1])   # True True
l2[0] += [8, 9]   # += 对于可变序列来说, 就是就地加
l2[1] += (8, 9)   # 注意!!! 这里是元组
print(l1)  # [[1, 2, 8, 9], (3, 4)]
print(l2)  # [[1, 2, 8, 9], (3, 4, 8, 9)]
print(l1[0] is l2[0], l1[1] is l2[1])   # True False

对可变的对象来说,如 l2[0] 引用的列表,+= 运算符 就地 修改列 表。

对元组来说,,然后重新绑定给变量 !

注意,一般来说,深复制不是件简单的事。如果对象有循环引用,那么 这个朴素的算法会进入无限循环。copy.deepcopy 函数会记住已经复制的对 象,因此能优雅地处理循环引用 `循环引用: `b 引用 a,然后追加到 a 中;deepcopy 会 想办法复制 a
a = [1, 2]
b = [a, 3]
a.append(b)
print(a)  # [1, 2, [[...], 3]]
print(b)  # [[1, 2, [...]], 3]

from copy import deepcopy
c = deepcopy(a)
print(c)  # [1, 2, [[...], 3]]
此外,深复制有时可能太深了。例如,对象可能会引用不该复制的外部 资源或单例值。我们可以实现特殊方法 __copy__() 和 __deepcopy__(),控制 copy 和 deepcopy 的行为 @[toc] ##函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参(call by sharing)
`共享传参`指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是

示例: 它在参数上调用 += 运算符。分别把 数字、列 表和元组 传给那个函数,实际传入的实参会以不同的方式受到影响。

函数可能会修改接收到的任何可变对象
def f(a, b):
    a += b
    return a

x = 1 
y = 2
z = f(x, y)   # x = 1   y = 2  没变
print(x is z)  # False

x = [1, 2]
y = [3, 4]
z = f(x, y)  # x = [1, 2, 3, 4]    y = [3, 4]
print(x is z)  # True   函数可能会修改接收到的任何可变对象

x = (1, 2)
y = (3, 4)
z = f(x, y)  # x = (1, 2)   y = (3, 4)
print(x is z)  # False

小结

每个python对象都有标识, 类型和值, 但只有值能变动

其实对象的类型也可以变动, 通过__class__, 不推荐使用

变量保存的是引用,这一点对 Python 编程有很多实际的影响。

  1. 简单的赋值不创建副本。

  2. 对 += 或 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。

  3. 为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑 定:现在变量绑定了其他对象。如果变量是之前那个对象的最后一 个引用,对象会被当作垃圾回收。

  4. 函数的参数以别名的形式传递,这意味着,函数可能会修改通过参 数传入的可变对象。这一行为无法避免,除非在本地创建副本,或 者使用不可变对象(例如,传入元组,而不传入列表)。

  5. 使用可变类型作为函数参数的默认值有危险,因为如果就地修改了 参数,默认值也就变了,这会影响以后使用默认值的调用。


仅当对象可变时, 对象标识才重要。
在 Python 中,用户定义的类,其实例默认可变(多数面向对象语言都是如此). 自己创建对象时,如果需要不可变的对象,一定要格外小心。
此时,对象的每个属性都必须是不可变的,否则会出现类似元组那种行为:元组本身不可变,但是如果里 面保存着可变对象,那么元组的值可能会变。

可变对象还是导致多线程编程难以处理的主要原因,因为某个线程 改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步 又会导致死锁。

猜你喜欢

转载自blog.csdn.net/qq_44810930/article/details/112411171
今日推荐