当前位置:首页>python>【Python】对象引用、可变性和垃圾回收

【Python】对象引用、可变性和垃圾回收

  • 2026-10-11 05:52:58
【Python】对象引用、可变性和垃圾回收

本篇文章部分内容来自《流畅的Python》

引用

和其他面向对象(也许是)的语言相比,Python 的变量究竟是什么?事实上,变量 = 标签:对象在堆上独立存在。变量只是贴在对象上的名字。

我们看一个例子:

a: dict[str, int] = {"val": 1}b: dict[str, int] = {"val": 1}print(a is b) # 结果为 Falseprint(a == b) # 结果为 True

在 Python 中,需要比较两个变量时,如果只是比较值,需要用==;如果是比较在内存中是否是同一个变量,需要用is

再看下面的例子:

a: dict[str, int] = {"val": 1}b = ab["val"] = 2print(a is b)print(a == b)print(a) # {'val': 2}

由于a和b绑定了同一个对象,所以对b的修改也会原封不动地加在a上。这行代码在C++中的行为完全不同,因为C++ 是"赋值即拷贝",而 Python 是"赋值即绑定":

std::map<std::string, int> a = {{"val", 1}};auto b = a;        // 🎯 调用拷贝构造函数,创建新对象b["val"] = 2;std::cout << ( &a == &b );  // false ← 不同对象std::cout << a["val"];      // 1 ← a 没变

可变性

Python 中的 dict 是个可变的类型,如果是不可变类型,比如 tuple,刚才的代码就无法执行了——因为 tuple 无法被修改,这时我们只能重新赋值:

a = (1,2)b = aprint(a is b) # Trueprint(a == b) # Trueb = (3,4)print(a is b) # Falseprint(a == b) # False

重新赋值意味着重新绑定了,此时b就是一个独立的 tuple,a和b自然无论是值还是内存地址都是不相等的。

浅复制

我不喜欢用拷贝这个词,因为我觉得音译很难让人理解。虽然,对于 mac 的用户来说,拷贝和复制是两回事(可惜我不用 mac )

最简单的是调用构造函数

l1 = [3, [55, 44], (7, 8, 9)]l2 = list(l1) print(l2) # [3, [55, 44], (7, 8, 9)]print(l2==l1) # Trueprint(l2 is l1) # False

当然,如果你记不住那么多构造函数,也可以使用全切片l2 = l1[:]

由于浅复制仅将标识复制过去,这意味着对l1中成员list的修改会直接影响到l2:

l1[1].remove(55)print(l1) # [3, [44], (7, 8, 9)]print(l2) # [3, [44], (7, 8, 9)]

如果这里是 tuple ,那就是另一回事了,由于 tuple 是不可变的,实际上会进行重新绑定:

l2[2] += (10, 11) print(l1) # [3, [44], (7, 8, 9)]print(l2) # [3, [44], (7, 8, 9, 10, 11)]

我们可以利用copy的copy()和deepcopy()进行显式的浅复制和深复制:

import copyl1 = [3, [55, 44], (7, 8, 9)]l2 = copy.copy(l1)l3 = copy.deepcopy(l1)l2[1].append(10)print(l1) # [3, [55, 44, 10], (7, 8, 9)]print(l2) # [3, [55, 44, 10], (7, 8, 9)]print(l3) # [3, [55, 44], (7, 8, 9)]

值得一提的是,Python不保证百分百进行浅复制(CPython),对于一些不可变变量和字面量(字符串和一些特定整数),Python不会仅因为复制行为就在内存中分配单独的空间,这种设计称为驻留:

t1 = (1,2,3)t2 = t1[:]print(t1 is t2)t3 = 42t4 = 42print(t3 is t4)t5 = "33"t6 = "33"print(t5 is t6)

这三个表达式的结果都是True

对于整数,Python 预先创建了一个包含 -5 到 256 的整数对象池。对于字符串,符合以下条件的字符串通常会被自动驻留:

  1. 标识符:变量名、函数名等(a = “var_name”)。
  2. 字面量:代码中直接写的字符串(s = “hello”)。
  3. 长度较短:通常长度为 1 的字符串。
  4. 仅包含字母、数字、下划线:类似标识符的字符串。

如果是运行时计算出来的字符串,通常不会自动驻留。可以使用 sys.intern() 强制将字符串放入驻留池:

import sysa = sys.intern("hello_" + "world")b = sys.intern("hello_" + "world")print(a is b)  # True ← 强制复用

不过,尽量不要依赖这种设计,尤其永远不要依赖 is 来判断值是否相等。

参数

基于上述的内容,Python 将标识传入函数时,也就意味着将可变变量的修改权力交给函数:

def changel(l):    l +=  [1, 2, 3]def changet(t):    t += (1, 2, 3)l1 = [4,5]changel(l1)print(l1) # [4, 5, 1, 2, 3]l2 = (4,5)changet(l2)print(l2) # (4, 5)

所以如果真的传入了可变变量,除非要显式修改变量,否则建议只使用拷贝。

def add_tax(prices, rate):    # ⚠️ 直接修改了传入的列表!调用者会意外发现自己的数据变了    for i in range(len(prices)):        prices[i] *= (1 + rate)    return pricesoriginal_prices = [100, 200]new_prices = add_tax(original_prices, 0.1)print(original_prices)  # [110.0, 220.0] ← 糟了!原数据被污染了

另外,不要给可变变量设置默认值,因为默认参数在函数定义时只创建一次,所有调用共享同一个对象:

# ⚠️ 默认列表在定义时创建一次,所有实例共享def add_student(name, students=[]):      students.append(name)    return studentsclass1 = add_student("Alice")print(class1)  # ['Alice']class2 = add_student("Bob")  print(class2)  # ['Alice', 'Bob'] ← 糟了!Bob 的班里出现了 Alice

正确的写法应该是:

# ✅ 每次调用时检查,如果是 None 则创建新列表def add_student(name, students=None):    if students is None:        students = []  # 每次调用都创建新列表    students.append(name)    return studentsclass1 = add_student("Alice")print(class1)  # ['Alice']class2 = add_student("Bob")print(class2)  # ['Bob'] ← 干净独立

垃圾回收

CPython的gc主要依赖引用计数,因此如果希望某个对象被回收掉,可以使用del删除引用:

class Person:    def __init__(self, name):        self.name = name        print(f"👤 创建 {name}")    def __del__(self):        print(f"💀 销毁 {self.name}")p1 = Person("Alice")p2 = p1  # p2 也指向同一个对象print(f"p1 is p2: {p1 is p2}")  # Truedel p1  # 只是删除 p1 这个名称绑定print("执行 del p1 后...")print(f"p2.name: {p2.name}")  # ✅ 仍然可以访问,对象未销毁print("执行 del p2 后...")  # 此时引用计数为 0,对象被销毁del p2  # 最后一个引用删除

但是某些情况下,我们想引用一个对象,但又不希望这个对象仅因为我们的引用而不被回收掉,此时可以使用弱引用:

p3 = Person("Bob")weak_p3 = weakref.ref(p3)  # 创建弱引用print(f"p3 存在时,weak_p3(): {weak_p3()}")  # <Person object>print(f"引用计数: {sys.getrefcount(p3) - 1}")  # -1 因为 getrefcount 本身会增加一次del p3  # 删除强引用print("执行 del p3 后...")print(f"weak_p3(): {weak_p3()}")  # None ← 对象已被回收

最新文章

随机文章