本篇文章部分内容来自《流畅的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 的整数对象池。对于字符串,符合以下条件的字符串通常会被自动驻留:
- 标识符:变量名、函数名等(a = “var_name”)。
- 字面量:代码中直接写的字符串(s = “hello”)。
如果是运行时计算出来的字符串,通常不会自动驻留。可以使用 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 ← 对象已被回收