一、变量不是盒子
Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。
下面代码中,无法使用“变量是盒子”来解释。
# 创建列表 [1, 2, 3],绑定变量 a。
a = [1, 2, 3]
# 变量 b 绑定 a 引用的值
b = a
# 修改 a 引用的列表,追加一项
a.append(4)
# 通过变量 b 可以看出效果。如果你认为 b 是一个盒子,存储盒子 a 中 [1, 2, 3] 的副本,那么这个行为就说不通了。
print(b) # [1, 2, 3, 4]
下图说明了在 Python 中为什么不能使用盒子来比喻变量,而便利贴才是变量的真正用途。

把变量想象为盒子,无法解释 **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。下面是推荐的写法:
x is None
否定的正确写法:
x 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 = (1, 2, [30, 40])
# 构建元组 t2,所含的项与 t1 一样
t2 = (1, 2, [30, 40])
# 虽然 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, [55, 44], (7, 8, 9)]
# 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 模块提供的copy
和deepcopy
函数分别对任意对象做浅拷贝和深拷贝。
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__()
,来控制 copy
和 deepcopy
的行为。
三、函数的参数是引用时
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 = [1, 2]
# 变量 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