Python深入-6-对象引用

一、变量不是盒子

Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。
下面代码中,无法使用“变量是盒子”来解释。

# 创建列表 [1, 2, 3],绑定变量 a。
a = [123]
# 变量 b 绑定 a 引用的值
b = a
# 修改 a 引用的列表,追加一项
a.append(4)
#  通过变量 b 可以看出效果。如果你认为 b 是一个盒子,存储盒子 a 中 [1, 2, 3] 的副本,那么这个行为就说不通了。
print(b)  # [1, 2, 3, 4]

下图说明了在 Python 中为什么不能使用盒子来比喻变量,而便利贴才是变量的真正用途。

Python深入-6-对象引用
image.png


把变量想象为盒子,无法解释 **Python **中的赋值。应该把变量视作便利贴,这样就好解释上图中的行为了。
因此,b = a 语句不是把 a 盒子中的内容复制到 b 盒子中,而是在标注为 a 的对象上再贴一个标注 b。
在 Python 中,赋值语句x = ...把名称 x 绑定到右边创建或引用的对象上。在绑定名称之前,对象必须存在。


为了理解 Python 中的赋值语句,应该始终先读右边。对象先在右边创建或获取,然后左边的变量才会绑定到对象上,就像给对象贴上标签一样。忘掉盒子吧!

因为变量只不过是标注,所以即使为对象贴上多个标注也没关系。多出来的标注就是别名.

二、同一性、相等性和别名

zs = {"name"'zs''age'20}
ls = zs
print(ls is zs)  # True
print(id(zs), id(ls))  # 2230300473472 2230300473472

对象一旦创建,标识始终不变。可以把标识理解为对象在内存中的地址。is 运算符比较两个对象的标识,id() 函数返回对象标识的整数表示。
对象 ID 的真正意义取决于具体实现。在 CPython 中,id()返回对象的内存地址,但是在其他 Python 解释器中可能是别的值。关键是,ID 一定是唯一的整数标注,而且在对象的生命周期内绝不会变。
实际编程中很少使用id()函数。对象的标识最常使用 is 运算符比较,无须直接调用 id() 函数。

2.1 在 `==` 和 `is` 之间选择

== 运算符比较两个对象的值(对象存储的数据),而 is比较对象的标识。
编程时,我们关注的通常是值,而不是标识,因此在 Python 代码中==出现的频率比is高。
然而,比较一个变量和一个单例时,应该使用 is。目前,最常使用 is 检查变量绑定的值是不是 None。下面是推荐的写法:

is None

否定的正确写法:

is not None

None 是最常使用 is 测试的单例。哨符对象也是单例,同样使用 is 测试。
is运算符比== 速度快,因为它不能重载,所以 Python 不用寻找要调用的特殊方法,而是直接比较两个整数 ID。其实,a == b 是语法糖,等同于 a.__eq__(b)。继承自 object 的__eq__ 方法比较两个对象的 ID,结果与 is 一样。但是,多数内置类型使用更有意义的方式覆盖了__eq__ 方法,把对象的属性值纳入考虑范围。

2.2 元组的相对不可变性

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

# t1 不可变,但是 t1[-1] 可变
t1 = (12, [3040])
# 构建元组 t2,所含的项与 t1 一样
t2 = (12, [3040])
# 虽然 t1 和 t2 是不同的对象,但是二者相等——与预期相符。
print(t1 == t2)  # True
#  查看 t1[-1] 列表的标识
print(id(t1[-1]))  # 2897188550080
# 就地修改 t1[-1] 列表
print(t1[-1].append(99))  # None
print(t1)  # (1, 2, [30, 40, 99])
# t1[-1] 的标识没变,只是值变了。
print(id(t1[-1]))  # 2598529136064
# 现在,t1 和 t2 不相等。
print(t1 == t2)  # False

2.3 默认做浅拷贝

复制列表(或多数内置的可变容器)最简单的方式是使用内置的类型构造函数

l1 = [3, [5544], (789)]
# list(l1) 创建 l1 的副本
l2 = list(l1)  # [3, [55, 44], (7, 8, 9)]
print(l2)
# 副本与源列表相等
print(l2 == l1)  # True
# 但是二者指代不同的对象
print(l2 is l1)  # False

对列表和其他可变序列来说,还可以使用简洁的 l2 = l1[:] 语句创建副本。
然而,构造函数或 [:]做的是浅拷贝(即复制最外层容器,副本中的项是源容器中项的引用)。如果所有项都是不可变的,那么这种行为没有问题,而且还能节省内存。但是,如果有可变的项,可能就会导致意想不到的问题。
**为任意对象做浅拷贝和深拷贝 **
浅拷贝通常来说没什么问题,但有时我们需要的是深拷贝(即副本不共享内部对象的引用)。copy 模块提供的copydeepcopy函数分别对任意对象做浅拷贝和深拷贝。

import 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)


bus1 = Bus(['Alice''Bill''Claire''David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)
# 使用 copy 和 deepcopy 创建 3 个不同的 Bus 实例
print(id(bus1), id(bus2), id(bus3))  # 2042812598224 2042812598352 2042812598288
# bus1 中的 'Bill' 下车后,bus2 中也没有他了
bus1.drop('Bill')
print(bus2.passengers)  # ['Alice', 'Claire', 'David']
# 查看 passengers 属性后发现,bus1 和 bus2 共享同一个列表对象,因为 bus2 是 bus1 的浅拷贝副本。
print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))  # 2042809802368 2042809802368 2042812518208
# bus3 是 bus1 的深拷贝副本,因此它的 passengers 属性引用另一个列表
print(bus3.passengers)  # ['Alice', 'Bill', 'Claire', 'David']

注意,一般来说,深拷贝不是一件简单的事。如果对象有循环引用,那么简单的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用。
另外,深拷贝有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例。可以实现特殊方法__copy__()__deepcopy__(),来控制 copydeepcopy 的行为。

三、函数的参数是引用时

Python 唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言采用这一模式,包括JavaScript、Ruby 和 Java(Java 的引用类型是这样,原始类型按值传参)。共享传参指函数的形参获得实参引用的副本。也就是说,函数内部的形参是实参的别名
这种模式的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(也就是说,不能把一个对象彻底替换成另一个对象)

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

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

3.2 防御可变参数

如果你定义的函数接收可变参数,那就应该谨慎考虑调用方是否期望修改传入的参数。
例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部?具体问题具体分析。这其实需要函数的编写者和调用方达成共识。

四、del 和垃圾回收

首先,你可能觉得奇怪,del 不是函数而是语句,写作 del x 而不是 del(x)。后一种写法也能起到作 用,但这仅仅是因为在 Python 中,x(x) 这两个表达式往往是同一个意思。
其次,del 语句删除引用,而不是对象。del 可能导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用时。重新绑定也可能导致对象的引用数量归零,致使对象被销毁。

# 创建对象 [1, 2],绑定变量 a。
a = [12]
# 变量 b 也绑定 [1, 2] 对象
b = a
# 删除引用 a。
del a
# [1, 2] 不受影响,因为还有 b 指向它。
print(b)
# 把 b 重新绑定另一个对象,[1, 2] 的最后一个引用随之删除。现在,垃圾回收程序可以销毁 [1, 2]了。
b = [3]

你可能听说过特殊方法 __del__,但是它不负责销毁实例,而且不应该在代码中调用。即将销毁实例时,Python 解释器调用 __del__ 方法,给实例最后的机会释放外部资源。自己编写的代码很少需要实现__del__ 方法,有些 Python 程序员会花时间实现,但吃力不讨好,因为 __del__ 方法不那么容易实现。详见《Python 语言参考手册》第 3 章中对特殊方法__del__的说明。
在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。
当引用计数归零时,对象立即被销毁:CPython 在对象上调用 __del__ 方法(如果定义了),然后释放分配给对象的内存。CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,那么即使再出色的引用方式也会导致组中的对象不可达。有些 Python 的实现,垃圾回收程序更复杂,不依赖引用计数,这意味着对象的引用计数为零时可能不会立即调用 __del__方法。


原文始发于微信公众号(Python之家):Python深入-6-对象引用

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198402.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!