Skip to content

对象与引用:身份、别名与可变性 ​

核心概念 ​

Python中一切皆对象,名字绑定到对象的引用。多个名字可以绑定到同一对象(别名),修改可变对象会影响所有别名。

关键概念:

  • 身份(identity):对象的唯一标识,用id()获取,is比较
  • 相等性(equality):对象的值是否相同,用==比较
  • 别名(aliasing):多个名字指向同一对象
  • 可变性(mutability):对象创建后能否修改

身份 vs 相等性 ​

python
a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)  # True(值相等)
print(a is b)  # False(不是同一对象)
print(a is c)  # True(同一对象)
print(id(a) == id(c))  # True

规则:

  • is 比较身份(内存地址)
  • == 比较值(调用__eq__方法)
  • 身份相同 → 相等
  • 相等 ≠ 身份相同

别名与可变对象 ​

python
list1 = [1, 2, 3]
list2 = list1  # list2 是 list1 的别名

list2.append(4)
print(list1)  # [1, 2, 3, 4],list1 也变了!

问题:修改list2影响了list1,因为它们是同一对象。

不可变对象无此问题 ​

python
a = "hello"
b = a
b = b + " world"  # 创建新字符串
print(a)  # "hello",a 不受影响

字符串不可变,b + " world"创建新对象,b重新绑定,a仍指向原对象。

浅拷贝 vs 深拷贝 ​

浅拷贝 ​

创建新容器,但元素仍是原对象的引用:

python
import copy

original = [[1, 2], [3, 4]]
shallow = copy.copy(original)  # 或 original.copy() 或 original[:]

shallow.append([5, 6])
print(original)  # [[1, 2], [3, 4]],不受影响

shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]],受影响!

解释:

  • shallow是新列表,追加元素不影响original
  • shallow[0]和original[0]是同一个列表对象,修改它影响双方

深拷贝 ​

递归复制所有嵌套对象:

python
deep = copy.deepcopy(original)

deep[0].append(999)
print(original)  # [[1, 2, 99], [3, 4]],不受影响
print(deep)     # [[1, 2, 99, 999], [3, 4]]

深拷贝创建完全独立的对象树。

何时用哪种 ​

  • 浅拷贝:容器元素是不可变对象,或不需要独立修改元素
  • 深拷贝:嵌套可变对象,需要完全独立的副本
  • 赋值:只需要别名,不需要副本

函数参数是引用传递 ​

python
def modify_list(lst):
    lst.append(4)

numbers = [1, 2, 3]
modify_list(numbers)
print(numbers)  # [1, 2, 3, 4]

函数接收的是对象引用,修改可变参数会影响调用者。

如何避免 ​

python
def safe_modify(lst):
    lst = lst.copy()  # 本地副本
    lst.append(4)
    return lst

numbers = [1, 2, 3]
result = safe_modify(numbers)
print(numbers)  # [1, 2, 3],不受影响
print(result)   # [1, 2, 3, 4]

或在调用时传递副本:

python
modify_list(numbers.copy())

默认参数陷阱再探 ​

python
def add_to(element, target=[]):
    target.append(element)
    return target

print(add_to(1))  # [1]
print(add_to(2))  # [1, 2],不是 [2]!

原因:默认值[]在函数定义时创建一次,所有调用共享。

查看默认值:

python
print(add_to.__defaults__)  # ([1, 2],)

修复:

python
def add_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

可变类型作为字典键 ​

python
# 错误
d = {}
key = [1, 2]
d[key] = "value"  # TypeError: unhashable type: 'list'

列表、字典、集合是可变的,不能作为字典键或集合元素。

可哈希类型(可作为键):

  • 不可变类型:int, float, str, tuple(元素也不可变), frozenset
  • 用户定义类(默认可哈希,除非定义了__eq__而未定义__hash__)
python
# 正确
d = {}
key = (1, 2)  # 元组
d[key] = "value"
print(d[(1, 2)])  # "value"

对象的生命周期 ​

Python 使用引用计数和垃圾回收管理内存。对象无引用时被回收:

python
a = [1, 2, 3]
b = a
del a  # 删除名字 a,但对象仍存在(b 引用它)
print(b)  # [1, 2, 3]

del b  # 现在对象无引用,可被回收

循环引用 ​

python
a = []
b = []
a.append(b)
b.append(a)  # 循环引用

Python 的循环垃圾回收器会处理循环引用。

练习 ​

  1. 预测输出:

    python
    x = [1, 2]
    y = [1, 2]
    z = x
    print(x == y, x is y, x is z)
  2. 别名陷阱:修复以下代码,使append_twice不影响原列表:

    python
    def append_twice(item, lst):
        lst.append(item)
        lst.append(item)
    
    numbers = [1, 2, 3]
    append_twice(4, numbers)
    print(numbers)  # 期望 [1, 2, 3]
  3. 浅拷贝问题:解释并修复:

    python
    matrix = [[0] * 3 for _ in range(3)]
    matrix[0][0] = 1
    print(matrix)  # 期望 [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
    
    # 错误写法
    bad_matrix = [[0] * 3] * 3
    bad_matrix[0][0] = 1
    print(bad_matrix)  # 实际输出什么?
  4. 深拷贝:编写函数independent_copy(nested_list),返回完全独立的副本。

  5. 可哈希类型:哪些可以作为字典键?[1, 2], (1, 2), {1, 2}, (1, [2, 3]), frozenset([1, 2])

完成练习后,打开本章答案页核对。

下一步 ​

理解了对象引用与可变性,下一节深入列表与元组:切片、方法、列表推导式。

回到第三章概览 · 返回Python学习路线

用心记录每一步成长