python对象的引用和可变性

Python对象的引用和可变性

1.变量是标签

当一个对象被绑定到一个变量上后,如果再将该变量绑定到另一个对象上,之前的引用不会受到影响
>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

变量a,b引用同一个列表 而不是创建新的副本

创建对象之后才会把变量分配给对象

>>> class Gizmo:
... def __init__(self):
... print('Gizmo id: %d' % id(self)) ...
>>> x = Gizmo()
Gizmo id: 4301489152 ➊
>>> y = Gizmo() * 10 ➋
Gizmo id: 4301489432 ➌
Traceback (most recent call last):
  File "", line 1, in 
  TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int' >>>
>>> dir() ➍
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']

 ❶ 输出的 Gizmo id: ... 是创建 Gizmo 实例的副作用。
 ❷ 在乘法运算中使用 Gizmo 实例会抛出异常。
 ❸ 这里表明,在尝试求积之前其实会创建一个新的 Gizmo 实例。
 ❹ 但是,肯定不会创建变量 y,因为在对赋值语句的右边进行求值时抛出了异常

证明现有对象后贴标签,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注

变量是标注所以无法阻止为对象贴上多个标注。贴的多个标注,就是别名

2.标识相等行别名

>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832} >>> lewis = charles ➊
>>> lewis is charles
True
>>> id(charles), id(lewis) ➋ (4300473992, 4300473992)
>>> lewis['balance'] = 950 ➌
>>> charles
{'name': 'Charles L. Dodgson', 'balance': 950, 'born': 1832}
❶ lewis 是 charles 的别名。
❷ is 运算符和 id 函数确认了这一点。
❸ 向 lewis 中添加一个元素相当于向 charles 中添加一个元素。

3.== 和is之前区别

== 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识。

通常,我们关注的是值,而不是标识,因此 Python 代码中 == 出现的频率比 is 高。

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

4.元祖的相对不可变性

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

>>> t1 = (1, 2, [30, 40]) ➊ 
>>> t2 = (1, 2, [30, 40]) ➋
>>> t1 == t2 ➌ True
>>> id(t1[-1]) ➍ 4302515784
>>> t1[-1].append(99) ➎ >>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) ➏ 4302515784
>>> t1 == t2 ➐ False
❶ t1 不可变,但是 t1[-1] 可变。
❷ 构建元组 t2,它的元素与 t1 一样。
❸ 虽然 t1 和 t2 是不同的对象,但是二者相等——与预期相符。 ❹ 查看 t1[-1] 列表的标识。
❺ 就地修改 t1[-1] 列表。
❻ t1[-1] 的标识没变,只是值变了。
❼ 现在,t1 和 t2 不相等。

5.默认的浅复制

pythontuor交互网址

l1 =  [3,[66,55,44],(7,8,9)]
l2 = list(l1)
l1.append(100)
l1[1].remove(55)
print('l1:',l1)
print('l2:',l2)
l2[1] += [33,22]
l2[2] += (10,11)
print(l1)
print(l2)

对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在l1[1] 中也有体现,因为它是 l2[1] 的别名。

对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于 l2[2] = l2[2] + (10, 11)。现在,l1 和 l2 中最后位置上的元组不是同一个对 象。

6.为任意对象做深复制和浅复制

深复制(即副本不共享内部对象的引用)。

copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。

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)
(4301498296, 4301499416, 4301499752) ➊
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David'] ➋
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800) ➌
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David'] 
❶ 使用 copy 和 deepcopy,创建 3 个不同的 Bus 实例。 ❷ bus1 中的 'Bill' 下车后,bus2 中也没有他了。
❸ 审查 passengers 属性后发现,bus1 和 bus2 共享同一个列表对象,因为 bus2 是 bus1 的浅复制副本。
❹ bus3 是 bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。

注意,一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法 会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引

7.python对比可辨类型的细节

对元组 t 来说,t[:] 不创建副本,而是返回同一个对象的引用。此 外,tuple(t) 获得的也是同一个元组的引用

>>> t1 = (1, 2, 3) 
>>> t2 = tuple(t1) 
>>> t2 is t1 ➊ 
True
>>> t3 = t1[:]
>>> t3 is t1 ➋ 
True
❶ t1 和 t2 绑定到同一个对象。 ❷t3也是。

str、bytes 和 frozenset 实例也有这种行为。注意,frozenset 实例不是序列, 因此不能使用 fs[:](fs 是一个 frozenset 实例)。但是,fs.copy() 具有相同 的效果:它会欺骗你,返回同一个对象的引用,而不是创建一个副本

字符串字面量可能会创建共享的对象

>>> t1 = (1, 2, 3) 
>>> t3 = (1, 2, 3) #➊
>>> t3 is t1 # ➋ 
False
>>> s1 = 'ABC'
>>> s2 = 'ABC' # ➌
>>> s2 is s1 # ➍ 
True
❶ 新建一个元组。
❷ t1 和 t3 相等,但不是同一个对象。
❸ 再新建一个字符串。
❹ 奇怪的事发生了,a 和 b 指代同一个字符串。
共享字符串字面量是一种优化措施,称为驻留(interning)。CPython 还会在小的整数 上使用这个优化措施,防止重复创建“热门”数字,如 0、-1 和 42。注意,CPython 不会 驻留所有字符串和整数,驻留的条件是实现细节,而且没有文档说明。

千万不要依赖字符串或整数的驻留!比较字符串或整数是否相等时,应该 使用 ==,而不是 is。驻留是 Python 解释器内部使用的一个特性。

总结

如果两个变量指代的不可变对象具有相同的值(a == b 为 True),实际上它们指代 的是副本还是同一个对象的别名基本没什么关系,因为不可变对象的值不会变,但有一 个例外。这里说的例外是不可变的集合,如元组和 frozenset:如果不可变集合保存 的是可变元素的引用,那么可变元素的值发生变化后,不可变集合也会随之改变。实际 上,这种情况不是很常见。不可变集合不变的是所含对象的标识。

变量保存的是引用,这一点对 Python 编程有很多实际的影响。 简单的赋值不创建副本。

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

def ldsx(x,y):
    x += y
    print(id(x))
    return x
a=[1,2,3]
print(id(a))
b=[1,1]
w = ldsx(a,b)
如a为列表可变对象则修改值,a,b为不可变对象 元祖或数字则创建新对象

为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定:现在变量绑定
了其他对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对
象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(例如,传入
元组,而不传入列表)。
使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也
就变了,这会影响以后使用默认值的调用。

在 CPython 中,对象的引用数量归零后,对象会被立即销毁。如果除了循环引用之外没 有其他引用,两个对象都会被销毁。某些情况下,可能需要保存对象的引用,但不留存 对象本身。例如,有一个类想要记录所有实例。这个需求可以使用弱引用实现,这是一 种低层机制,是 weakref 模块中 WeakValueDictionary、WeakKeyDictionary 和 WeakSet 等有用的集合类,以及 finalize 函数的底层支持。

你可能感兴趣的:(python笔记,python)