第六章 对象引用、可变性和垃圾回收

引用与绑定

传统“变量是盒子”的比喻具有误导性,尤其在面向对象语言(如 Java、Python)中。更准确的模型是“变量是贴在对象上的标签(便利贴)”,变量名只是对对象的引用,而非容器。

# 展示变量不是一个盒子而是一个标签
a = [1, 2, 3]        # 创建列表对象,并将标签 'a' 贴到该对象上
b = a                # 将标签 'b' 也贴到同一个列表对象上
a.append(4)          # 通过标签 'a' 修改对象内容
print(b)             # 输出: [1, 2, 3, 4]

b = a 并未复制列表,而是让 ba 指向同一个对象。若按“盒子”模型,会误以为 b 拥有独立副本,无法解释为何修改 a 会影响 b。“便利贴”模型清晰说明:两个标签贴在同一对象上,修改对象内容自然对所有标签可见。

赋值的本质是"绑定"。Python 的赋值语句 x = ... 的含义是将名称 x 绑定到右侧表达式所创建或引用的对象。对象必须先存在,才能被绑定。赋值语句右侧先求值,左侧再绑定。

class Gizmo:
    def __init__(self):
        print(f'Gizmo id: {id(self)}')

x = Gizmo()          # 成功创建实例,绑定到 x
# 输出: Gizmo id: 4301489152

# 尝试创建第二个实例并执行非法操作
try:
    y = Gizmo() * 10
except TypeError as e:
    pass  # 捕获异常,避免程序中断

# 查看当前命名空间
print([name for name in dir() if not name.startswith('__')])
# 输出包含 'Gizmo' 和 'x',但不包含 'y'

Gizmo()* 10 之前已执行,因此会打印 Gizmo id: ...,说明对象已创建。但由于 Gizmo 实例不支持 * 运算,右侧表达式求值失败,抛出 TypeError变量 y 从未被绑定,因为赋值语句左侧的绑定操作在右侧求值失败后不会执行。这证明:对象创建发生在绑定之前,且绑定仅在右侧成功求值后才发生。

身份、相等性与别名

**身份(Identity)**是对象在内存中的唯一标识,创建后永不改变。可通过 id() 获取(CPython 中为内存地址),用 is 运算符比较。**相等性(Equality)**指的是对象的值是否相同,由 == 运算符判断,实际调用 __eq__() 方法。**别名(Aliasing)**指的是多个变量绑定到同一个对象。

# 引用同一个对象

charles = {'name': 'Charles L. Dodgson', 'born': 1832}
# 未创建新对象,仅新增一个标签
lewis = charles          # lewis 是 charles 的别名
print(lewis is charles)  # True
print(id(charles), id(lewis))  # 输出相同 ID,如 (4300473992, 4300473992)
lewis['balance'] = 950   # 通过 lewis 修改对象
print(charles)           # {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
# 值相等但身份不同

alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
print(alex == charles)    # True(值相等)
print(alex is not charles)  # True(身份不同)

一个对象的身份在其创建后永远不会改变;你可以将其视为对象在内存中的地址。is 运算符用于比较两个对象的身份;id() 函数返回一个代表该身份的整数。

is== 的使用原则

== 运算符比较对象的(即它们所包含的数据),而 is 比较的是对象的身份

优先使用 ==,因为绝大多数场景关心值是否相等。

仅在以下情况使用 is,如比较单例对象(如 Nonex is Nonex is not None;或者比较哨兵对象(sentinel)如 END_OF_DATA = object()

但二者存在性能差异is 直接比较 ID,不可重载,速度更快;== 可能触发复杂的内容比较。除 None 和哨兵对象外,其他 is 用法通常错误。不确定时,应使用 ==

a == b 实际上是 a.__eq__(b) 的语法糖。从 object 继承而来的 __eq__ 方法默认比较对象 ID,因此结果与 is 相同。但大多数内置类型(如 dictlist)都重写了 __eq__,使其基于对象内容进行有意义的值比较。通常,我们更关注对象的相等性而非身份。检查 None 是使用 is 运算符唯一常见且正确的场景。我在

元组的相对不可变性

元组和大多数 Python 容器(如列表、字典、集合等)一样,都是容器:它们保存的是对其他对象的引用。相比之下,像 strbytesarray.array 这样的扁平序列并不包含引用,而是直接在连续内存中存储其内容(字符、字节或数字)。

容器保存的是对其他对象的引用。元组本身不可变(结构固定),但若包含可变对象(如列表),其“值”仍可变。不可变性仅保证元组所持引用不变,不保证引用对象的内容不变。

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
print(t1 == t2)          # True(初始值相等)
print(id(t1[-1]))        # 记录列表 ID,如 4302515784
t1[-1].append(99)        # 就地修改内部列表
print(t1)                # (1, 2, [30, 40, 99])
print(id(t1[-1]))        # ID 不变(仍为 4302515784)
print(t1 == t2)          # False(内容已不同)

元组 t1 的物理结构未变(仍引用同一列表),但列表内容变化导致 t1t2 不再相等。此特性影响元组的哈希性(含可变对象的元组不可哈希)。

默认进行浅复制

浅复制复制顶层容器,内部元素仍为原对象的引用;可通过list(original) 或切片 original[:]创建。但其存在风险,若内部含可变对象,修改会影响原对象。

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)            # 浅复制
l1.append(100)           # 修改 l1 顶层,不影响 l2
l1[1].remove(55)         # 修改共享的内部列表,影响 l2
l2[1] += [33, 22]        # 就地修改列表(+= 对可变对象是 in-place),影响 l1
l2[2] += (10, 11)        # 元组不可变,+= 创建新元组并重新绑定 l2[2],不影响 l1[2]
print('l1:', l1)
print('l2:', l2)
l1: [3, [66, 44], (7, 8, 9), 100]
l2: [3, [66, 44], (7, 8, 9)]
l1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
l2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

任意对象的深复制与浅复制

**浅复制(shallow copy)**指的是创建新容器对象,但内部元素仍为原对象的引用。深复制(deep copy)指的是递归复制整个对象及其所有嵌套对象,生成完全独立的副本

  • copy.copy(obj):浅复制
  • copy.deepcopy(obj):深复制
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)

构造函数中使用 list(passengers) 确保 self.passengers 是一个新列表,避免直接绑定外部传入的列表。

import copy

bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)        # 浅复制
bus3 = copy.deepcopy(bus1)    # 深复制

print((id(bus1), id(bus2), id(bus3)))
# 输出三个不同的 Bus 实例 ID

bus1.drop('Bill')             # 从 bus1 中移除 'Bill'

print(bus2.passengers)        # ['Alice', 'Claire', 'David']
print((id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)))
# bus1 和 bus2 的 passengers 是同一列表;bus3 的是独立副本
print(bus3.passengers)        # ['Alice', 'Bill', 'Claire', 'David']
  • copy.copy() 仅复制顶层对象(Bus 实例),但 passengers 列表仍共享。
  • copy.deepcopy() 递归复制所有层级,包括内部可变对象,实现完全隔离。

深复制处理循环运用

a = [10, 20]
b = [a, 30]
a.append(b)                   # a 引用 b,b 又引用 a,形成循环
print(a)                      # [10, 20, [...], 30]([...] 表示循环)

from copy import deepcopy
c = deepcopy(a)
print(c)                      # [10, 20, [[...], 30]](结构相同,但为全新对象)

deepcopy() 内部维护一个 memo 字典记录已复制对象,避免无限递归。即使存在循环引用,也能正确生成结构等价但身份独立的副本。

使用深复制时应该避免过度复制风险,某些对象(如文件句柄、单例、数据库连接)不应被复制。

可通过实现以下特殊方法控制复制逻辑:

  • __copy__(self):定义浅复制行为
  • __deepcopy__(self, memo):定义深复制行为

函数参数即引用

Python 唯一的参数传递模式是“共享传参”,函数的形式参数获得实参引用的副本,即参数成为实参的别名。这导致函数可以修改传入的可变对象(如列表、字典),但是函数无法改变对象的身份(即不能用新对象完全替换原对象)。

# 不同对象类型在 += 操作下的行为差异
def f(a, b):
    a += b
    return a

# 不可变对象:int
x = 1
y = 2
result = f(x, y)
print((x, y))  # (1, 2) — x 未改变

# 可变对象:list
a = [1, 2]
b = [3, 4]
result = f(a, b)
print((a, b))  # ([1, 2, 3, 4], [3, 4]) — a 被就地修改

# 不可变对象:tuple
t = (10, 20)
u = (30, 40)
result = f(t, u)
print((t, u))  # ((10, 20), (30, 40)) — t 未改变

莫将可变对象作为默认参数值

默认值在函数定义时求值一次,成为函数对象的属性(存储在 __defaults__ 中)。若默认值为可变对象,所有未传参的调用将共享同一对象,导致隐蔽的副作用。

# 一个错误实现
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.pick('Charlie')
bus1.drop('Alice')
print(bus1.passengers)  # ['Bill', 'Charlie']

# 使用默认值 暴露问题
bus2 = HauntedBus()
bus2.pick('Carrie')
bus3 = HauntedBus()
print(bus3.passengers)  # ['Carrie'] — 默认列表已被修改
bus3.pick('Dave')
print(bus2.passengers)  # ['Carrie', 'Dave'] — bus2 与 bus3 共享列表
print(bus2.passengers is bus3.passengers)  # True

# 验证默认值被污染
print(HauntedBus.__init__.__defaults__)  # (['Carrie', 'Dave'],)
print(HauntedBus.__init__.__defaults__[0] is bus2.passengers)  # True

正确做法应该使用 None 作为默认值,并在函数内创建新对象。

def __init__(self, passengers=None):
    if passengers is None:
        self.passengers = []
    else:
        self.passengers = list(passengers)  # 创建副本

防御可变参数

在使用可变参数之前应该明确调用者是否期望传入的参数被修改最佳实践是在将可变参数赋值给实例变量前,创建副本

# 一个错误实现
class TwilightBus:
    """一辆会让乘客消失的校车"""
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = passengers  # 错误:直接绑定,形成别名

    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')
print(basketball_team)  # ['Sue', 'Maya', 'Diana'] — 原始列表被修改
# 正确实现
def __init__(self, passengers=None):
    if passengers is None:
        self.passengers = []
    else:
        self.passengers = list(passengers)  # 创建副本,支持任意可迭代对象

这样做可以避免副作用,内部操作不影响外部数据。还可以提高灵活性,接受任意可迭代对象(列表、元组、集合等)并且符合“最小惊讶原则”,用户不会因副作用感到意外。虽然复制操作有性能开销,但相比隐蔽 bug,通常值得付出。

除非方法明确设计为修改传入的对象,否则在将参数直接赋值给实例变量前务必三思

del 与垃圾回收

del 是语句,不是函数:应写作 del x,而非 del(x)del 删除的是引用,而非对象本身,仅当对象的引用计数降为零(且无循环引用阻碍)时,垃圾回收器才会销毁对象。对象销毁由垃圾回收器自动管理,Python 不提供显式销毁机制。

a = [1, 2]      # 创建对象,绑定到 a
b = a           # b 成为同一对象的别名
del a           # 删除 a 引用;对象仍存在(b 指向它)
b = 'new'       # b 重新绑定,原列表失去所有引用 → 可被回收

__del__

用于在实例即将被销毁时由解释器调用,用于释放外部资源(如文件句柄);但其实日常用不到太多。优先使用上下文管理器(with)或显式 close() 方法管理资源。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐