news 2026/10/16 5:36:28

Python对象引用机制详解:告别‘指针’误区

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python对象引用机制详解:告别‘指针’误区

1. 项目概述:Python里没有“指针”,但人人都在用它

“理解pointer in Python”这个标题乍看有点奇怪——Python官方文档里压根不提pointer这个词,类型系统里也没有指针类型,变量赋值不会产生内存地址拷贝,&和*操作符根本不存在。可为什么成千上万的开发者,尤其是从C/C++转过来的、或者刚学完数据结构的初学者,会反复搜索“Python pointer”?我带过十几期编程训练营,几乎每期都有学员举手问:“老师,Python里怎么取变量的地址?”“列表传参是传值还是传指针?”“为什么修改函数里的list,外面的也变了?”——这些问题背后,不是语法困惑,而是认知模型的错位:他们带着C语言的指针心智模型,一头撞进Python的对象引用世界,结果发现所有直觉都失效了。

这恰恰是本篇要彻底厘清的核心:Python没有pointer,但它有一套更精巧、更一致、也更容易误读的对象引用机制(object reference model)。关键词“pointer in Python”本质是个误导性热词,是搜索引擎把用户困惑当需求的结果;真正该掌握的,是Python如何管理对象、变量、内存和别名。它决定了你能否写出无副作用的函数、能否安全地共享大型数据结构、能否避免浅拷贝引发的幽灵bug、甚至影响你对is与==、id()、copy.deepcopy()的理解深度。适合三类人:刚学完Python基础想突破瓶颈的新手;写过几年代码但总在可变对象传参上栽跟头的中级开发者;以及需要向团队讲清楚“为什么这段代码改了全局变量”的技术布道者。这不是语法复习,而是一次底层模型的重装。

2. 核心设计逻辑:为什么Python选择引用模型而非指针模型?

2.1 从C的指针到Python的引用:一次根本性的范式迁移

要真正理解Python的“pointer-like behavior”,必须先看清C语言指针的本质。在C中,指针是一个存储内存地址的变量,它本身有类型(如int*),可以进行算术运算(p++),可以解引用(*p),可以为空(NULL),甚至可以指向栈、堆或只读段。它的存在是为了让程序员直接操控内存布局——这是高效和灵活的代价,也是段错误、野指针、内存泄漏的根源。

Python彻底放弃了这条路。它的设计哲学第一条就是“简单优于复杂”,而指针正是复杂性的集大成者。取而代之的是统一的对象引用模型(Uniform Object Reference Model)。这个模型有三个不可动摇的基石:

  1. 一切皆对象(Everything is an object):数字、字符串、函数、模块、甚至类型本身,都是PyObject*结构体的实例。它们被分配在堆上,由Python解释器统一管理生命周期。
  2. 变量即标签(Variables are names):x = 42并非把42存入名为x的盒子,而是创建一个名为x的标签,贴在整数对象42上。x本身不占用存储数值的空间,它只是一个指向对象的引用(reference)。
  3. 引用不可变(References are immutable):你无法像C那样对引用做加减运算,也无法获取其底层地址并手动偏移。你能做的只有两件事:让标签指向另一个对象(x = y),或者让对象自身发生变化(如果它是可变的)。

提示:这个模型不是为了“模拟指针”,而是为了解决指针带来的问题。Python不需要malloc/free,因为垃圾回收器(GC)自动追踪所有引用计数;不需要const修饰符,因为不可变对象(如int,str,tuple)天然线程安全;也不需要担心悬空指针,因为只要还有标签指向对象,它就不会被销毁。

2.2 引用模型如何自然导出“指针感”行为?

正是这三个基石,让Python在不提供指针语法的情况下,产生了所有让人联想到指针的现象。我们拆解几个最典型的场景:

  • “传引用”假象:当你把一个列表传给函数,函数内部修改lst.append(1),外部列表真的变了。这不是因为Python传递了“指针”,而是因为函数参数lst这个新标签,和调用方的标签,同时贴在了同一个列表对象上。修改的是对象本身(可变对象的特性),而不是标签的指向。如果传入的是字符串"hello",函数里执行text += " world",外部字符串不会变,因为+=对不可变对象意味着创建新对象并让text标签指向它,原标签text已失效。

  • is与==的分水岭:a is b判断两个标签是否指向同一个对象(内存地址相同),a == b判断两个对象的值是否相等。对于小整数(-5到256)和短字符串,CPython做了缓存优化,所以a = 100; b = 100; a is b为True,但这只是实现细节,不是语言规范。而[1,2] is [1,2]永远是False,因为每次[]都创建了新对象。这种差异,正是引用模型最直观的体现。

  • id()函数的真相:id(obj)返回的是对象在内存中的唯一标识符,在CPython中它确实是对象的内存地址。但请记住,这个地址是Python解释器内部使用的,你无法用它做任何指针运算。id()存在的唯一意义,是让你能验证两个变量是否指向同一对象,是调试引用关系的工具,而非暴露底层指针。

2.3 为什么这个设计比指针更安全、也更强大?

很多人觉得“没有指针”是Python的短板,实则不然。引用模型在三个关键维度上实现了降维打击:

  • 安全性:没有指针算术,就杜绝了越界访问;没有手动内存管理,就消除了use-after-free和double-free;所有对象通过引用计数+循环GC管理,内存泄漏概率极低。某公司曾将一个C++图像处理模块用Python重写,核心bug数量下降70%,其中大部分是内存相关错误。

  • 一致性:在C中,数组名、函数名、结构体成员访问,指针语义各不相同,新手要记一堆规则。Python里,obj.attr、obj[key]、obj.method(),底层都是通过对象的__dict__或__getattribute__查找,统一用引用传递。len(obj)、str(obj)、list(obj)这些内置函数,背后都是调用对象的__len__、__str__、__iter__方法,所有交互都基于对象协议,而非内存布局。

  • 表达力:引用模型天然支持高阶函数和闭包。def make_adder(x): return lambda y: x + y中,内层lambda能捕获外层函数的局部变量x,正是因为x是一个指向对象的引用,而这个引用被闭包环境保存下来。在C中实现类似功能,你需要手动管理函数对象的生命周期和捕获变量的内存,复杂度指数级上升。

3. 关键细节解析:引用、别名、可变性与拷贝的四重奏

3.1 变量、对象与引用:一张图说清三者关系

想象一个Python程序运行时的内存世界:它由两大部分组成——堆(Heap)和命名空间(Namespace)。

  • 堆:一片巨大的、由Python解释器管理的内存区域。所有对象(int,list,dict,function等)都诞生于此,并在此安息。每个对象都有一个唯一的id(内存地址)、一个类型(type)和一个引用计数(refcount)。

  • 命名空间:可以理解为一个字典(dict),键是变量名(如'x','my_list'),值是指向堆中某个对象的引用。全局命名空间、函数局部命名空间、类命名空间,都是这样的字典。

当执行x = [1, 2, 3]时,发生了三件事:

  1. 解释器在堆上创建一个新的list对象,内容为[1,2,3],分配内存,计算其id(比如140234567890123),初始引用计数为1。
  2. 在当前命名空间(比如全局)中,创建一个键值对:'x' -> 140234567890123。这里的140234567890123不是数字,而是解释器内部用来定位对象的句柄,你可以把它理解为一个“门牌号”。

再执行y = x,只发生一件事:

  1. 在命名空间中,创建新键值对:'y' -> 140234567890123。注意,没有创建新列表,没有复制数据,只是多贴了一个标签。此时,该列表对象的引用计数变为2。

这就是“别名(aliasing)”:x和y是同一个列表对象的两个不同名字。修改x.append(4),y看到的也是[1,2,3,4],因为它们指向同一个物理内存块。

注意:x = x + [4]和x += [4]效果天壤之别。前者是+操作符,对列表返回一个新对象,x标签被重新指向新列表,原列表引用计数减1;后者是+=,对可变序列是就地修改(in-place mutation),x标签依然指向原列表,只是内容变了。这是引用模型下最易踩的坑之一。

3.2 可变对象(Mutable)与不可变对象(Immutable):引用模型的分水岭

Python对象的可变性,是理解引用行为的钥匙。它不取决于你是否“想”修改它,而取决于对象类型的设计。

  • 不可变对象(Immutable):int,float,str,tuple,frozenset。它们的定义是:一旦创建,其内容(value)就不能被改变。尝试修改,只会创建一个新对象。

    • a = "hello"; b = a; a += " world"→b仍是"hello",a现在指向新字符串"hello world"。a和b的id不同。
    • x = (1, 2); y = x; x += (3,)→y仍是(1,2),x是(1,2,3)。元组的+=是创建新元组。
  • 可变对象(Mutable):list,dict,set,bytearray。它们的定义是:提供了就地修改其内容的方法。

    • m = [1,2]; n = m; m.append(3)→n也变成[1,2,3],因为append()修改的是m所指向的那个列表对象本身。
    • d = {'a':1}; e = d; d['b'] = 2→e也包含{'a':1, 'b':2}。

关键洞察:“可变性”决定了对象是否能被“就地修改”,而“引用”决定了有多少个变量名在看着这个被修改的对象。两者叠加,才产生了所有“神奇”现象。一个常见误区是认为“不可变对象就没有引用问题”,其实不然:a = (1, [2,3]); b = a; a[1].append(4),b也会变成(1, [2,3,4]),因为元组本身不可变(不能替换其元素),但元组里的列表是可变对象,对其的修改是就地发生的。

3.3 拷贝的三种境界:浅拷贝、深拷贝与赋值

当需要“断开”两个变量间的引用关系时,拷贝(copy)是唯一出路。但Python提供了三种粒度,选错一种,后果严重。

拷贝方式语法/函数行为描述适用场景风险提示
赋值(=)b = a不是拷贝!只是创建新标签,指向同一对象。引用计数+1。90%的场景,当你确实想共享数据时。误以为是拷贝,导致意外的副作用。
浅拷贝(Shallow Copy)b = a.copy()
b = list(a)
b = a[:]
import copy; b = copy.copy(a)
创建一个新容器对象,但容器内的元素仍然是原对象的引用。容器内全是不可变对象(如list[int],list[str]),或你明确知道内部可变对象不会被修改。如果容器内有可变对象(如list[list]),修改内层列表,新旧容器都会受影响。
深拷贝(Deep Copy)import copy; b = copy.deepcopy(a)创建一个全新的对象树,递归地拷贝容器及其所有嵌套的子对象。容器结构复杂,且内部可变对象可能被独立修改。性能开销大(时间+内存),可能陷入无限递归(如自引用对象)。

实操案例:处理一个嵌套配置字典。

import copy # 原始配置 config = { 'database': {'host': 'localhost', 'port': 5432}, 'cache': {'enabled': True, 'ttl': 300} } # 错误:赋值 config_dev = config config_dev['database']['host'] = 'dev-db.example.com' # 糟糕!config也被改了 # 危险:浅拷贝 config_test = config.copy() # 或 dict(config) config_test['database']['host'] = 'test-db.example.com' # 依然糟糕!config['database']被改了 # 正确:深拷贝 config_prod = copy.deepcopy(config) config_prod['database']['host'] = 'prod-db.example.com' # 安全!config和config_test都不受影响

实操心得:我在某次线上事故复盘中发现,一个关键服务的配置被并发请求意外覆盖,根源就是用了dict.copy()去“隔离”配置,而配置字典里嵌套了数据库连接池对象。深拷贝虽慢,但在配置隔离、单元测试Mock数据准备、状态快照等场景,是唯一可靠的选择。性能敏感场景,可考虑用dataclasses.replace()或attrs.evolve()替代深拷贝,它们只拷贝被修改的字段。

3.4id(),is,==与hash():引用模型的四大探针

这四个内置函数/操作符,是调试引用关系的黄金组合,必须烂熟于心。

  • id(obj):返回对象的唯一标识符(CPython中为内存地址)。它是引用模型的“X光机”,能直接看到两个变量是否指向同一块内存。

    a = [1,2,3] b = a print(id(a) == id(b)) # True c = [1,2,3] print(id(a) == id(c)) # False,不同对象
  • is操作符:判断两个对象是否为同一对象,即id(a) == id(b)。它是id()的语法糖,语义清晰,性能最优。

    # 最佳实践:用`is`检查None、True、False等单例 if result is None: ... # ✅ 推荐 if result == None: ... # ❌ 不推荐,可能被重载
  • ==操作符:调用对象的__eq__()方法,判断值是否相等。它可被任意类型重载,语义由类作者定义。

    from dataclasses import dataclass @dataclass class Point: x: int y: int def __eq__(self, other): return self.x == other.x and self.y == other.y p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # True,值相等 print(p1 is p2) # False,不同对象
  • hash(obj):返回对象的哈希值,用于字典键和集合成员。只有不可变对象才有可靠的哈希值。可变对象(如list,dict)默认不可哈希,因为其值变化会导致哈希值变化,破坏哈希表结构。

    # 这会报错:TypeError: unhashable type: 'list' # my_set = {[1,2], [3,4]} # 但你可以用tuple代替 my_set = {(1,2), (3,4)} # ✅ tuple是不可变的

注意事项:不要试图用hash()来比较对象身份!hash(a) == hash(b)只说明哈希值相等(可能碰撞),绝不意味着a is b。hash()的唯一合法用途是作为字典键或集合成员。

4. 实操过程详解:从代码片段到模型验证的完整闭环

4.1 经典案例拆解:函数参数传递的“黑箱”

让我们用一个被问爆的代码片段,走一遍完整的分析流程:

def modify_list(lst): print(f"函数内,lst的id: {id(lst)}") lst.append("new item") # 就地修改 lst = ["reassigned"] # 重新赋值 print(f"函数内,lst重新赋值后id: {id(lst)}") my_list = ["original"] print(f"调用前,my_list的id: {id(my_list)}") modify_list(my_list) print(f"调用后,my_list的内容: {my_list}")

预期输出与逐行解析:

调用前,my_list的id: 140234567890123 函数内,lst的id: 140234567890123 # ✅ 同一对象,引用传入 lst.append("new item") # ✅ 修改原列表对象 lst = ["reassigned"] # ❌ 创建新列表,lst标签指向新对象 函数内,lst重新赋值后id: 140234567890456 # ✅ 新对象id 调用后,my_list的内容: ['original', 'new item'] # ✅ 外部列表被修改了!

关键结论:

  • Python的“参数传递”既不是传值(pass by value),也不是传引用(pass by reference),而是传对象引用(pass by object reference)。函数形参lst,是实参my_list所指向对象的一个新标签。
  • lst.append(...)修改的是那个共同指向的对象,所以外部可见。
  • lst = [...]是给lst这个局部变量重新贴标签,它切断了lst与原列表的联系,但my_list的标签依然牢牢贴在原列表上,因此外部不受影响。

实操心得:我见过太多人在这个地方栽跟头,尤其在写工厂函数或装饰器时。一个简单原则:如果函数需要修改传入的可变对象,请明确在文档中声明,并避免在函数内部对参数名重新赋值。更好的实践是,让函数返回一个新对象,由调用方决定是否赋值,这样语义更清晰,也更符合函数式编程思想。

4.2 动手实验:可视化引用关系的三步法

要真正内化引用模型,光看代码不够,得亲手“看见”它。我推荐一个零依赖的三步实验法:

第一步:用id()和is建立基线

# 创建几个对象 a = [1, 2] b = a c = [1, 2] print("a and b:", id(a), id(b), a is b) # True print("a and c:", id(a), id(c), a is c) # False print("b and c:", id(b), id(c), b is c) # False

第二步:引入可变性,观察“传染性”

# 修改a a.append(3) print("a:", a) # [1, 2, 3] print("b:", b) # [1, 2, 3] —— b也变了! print("c:", c) # [1, 2] —— c没变 # 修改b(b和a是同一个) b[0] = 99 print("a:", a) # [99, 2, 3] print("b:", b) # [99, 2, 3]

第三步:用copy打破传染链

import copy d = copy.copy(a) # 浅拷贝 e = copy.deepcopy(a) # 深拷贝 print("a and d:", a is d) # False print("a and e:", a is e) # False # 修改a a.append(4) print("a:", a) # [99, 2, 3, 4] print("d:", d) # [99, 2, 3] —— 浅拷贝,未受新append影响 print("e:", e) # [99, 2, 3] —— 深拷贝,也未受影响 # 但如果a里有可变对象... a_with_nested = [[1,2], [3,4]] f = copy.copy(a_with_nested) g = copy.deepcopy(a_with_nested) a_with_nested[0].append(3) # 修改内层列表 print("f:", f) # [[1,2,3], [3,4]] —— 浅拷贝的内层被“传染” print("g:", g) # [[1,2], [3,4]] —— 深拷贝完全隔离

这个实验的价值在于,它把抽象的“引用”概念,转化成了可测量(id)、可比较(is)、可操作(append)的具体行为。每一次print,都是对模型的一次验证。

4.3 高级技巧:利用weakref打破循环引用

引用模型有个潜在问题:循环引用(circular reference)。当两个对象互相持有对方的引用时,它们的引用计数永远不会降到0,导致CPython的引用计数GC无法回收它们,只能依赖更耗时的循环检测GC。虽然现代Python已大幅优化,但在内存敏感场景(如长期运行的服务、嵌入式Python),仍需警惕。

weakref模块提供了“弱引用”,它不会增加目标对象的引用计数,从而打破循环。

import weakref class Parent: def __init__(self, name): self.name = name self.children = [] def add_child(self, child): self.children.append(child) # 关键:用弱引用避免循环 child.parent_ref = weakref.ref(self) class Child: def __init__(self, name): self.name = name self.parent_ref = None # 弱引用存储 @property def parent(self): # 通过弱引用获取父对象,可能为None return self.parent_ref() if self.parent_ref else None # 创建对象 p = Parent("P1") c = Child("C1") p.add_child(c) print(c.parent.name) # "P1" print(p.children[0].name) # "C1" # 删除强引用 del p # 此时,Parent对象可以被立即回收,因为Child持有的是弱引用,不阻止其销毁

实操心得:weakref是高级武器,日常开发很少用到。但一旦遇到内存泄漏排查,它就是救命稻草。我曾在一个Web爬虫项目中,发现内存随时间线性增长,tracemalloc定位到是URL解析器缓存了大量BeautifulSoup对象,而这些对象又通过.parent属性反向引用了整个DOM树。用weakref.proxy包装.parent后,内存使用立刻回归平稳。记住:weakref不是银弹,它要求你主动管理“引用可能失效”的情况,比如在访问前用if ref(): obj = ref()做判空。

5. 常见问题与排查技巧实录:那些年我们踩过的引用坑

5.1 “为什么我的列表在函数里append了,外面却没变?”——最经典的误解

现象:写了如下代码,期望my_list被修改,但实际没变。

def bad_append(lst, item): lst = lst + [item] # 错误!创建了新列表 my_list = [1, 2] bad_append(my_list, 3) print(my_list) # [1, 2],没变!

原因分析:lst + [item]是+操作符,对列表返回一个新列表对象。lst = ...这行代码,只是让函数内的局部变量lst指向了这个新对象,而函数外的my_list标签,依然坚定地指向原来的列表。这是一个纯粹的赋值操作,与原列表无关。

正确解法:

def good_append(lst, item): lst.append(item) # ✅ 就地修改原对象 def also_good_append(lst, item): return lst + [item] # ✅ 返回新对象,由调用方决定 # 调用方式1 my_list = [1, 2] good_append(my_list, 3) # 直接修改 print(my_list) # [1, 2, 3] # 调用方式2 my_list = [1, 2] my_list = also_good_append(my_list, 3) # 重新赋值 print(my_list) # [1, 2, 3]

排查技巧:当你发现“修改没生效”,第一时间检查你用的是+=还是+,是.append()/.extend()还是+。打开Python Shell,输入help(list.append)和help(list.__add__),看它们的文档字符串,就能立刻分辨。

5.2 “为什么a = []; b = a; a += [1]; b += [2],结果a和b都是[1,2]?”——可变对象的共享陷阱

现象:两个变量看似独立,但修改其中一个,另一个也跟着变,而且变的不是同一个东西。

原因分析:a += [1]和b += [2]都是对同一个列表对象的就地修改。a和b是别名,+=操作符对可变序列(list,dict)的含义是__iadd__,它修改的是对象自身,而不是创建新对象。所以a += [1]后,列表变成[1];紧接着b += [2],因为b指向的还是那个列表,所以它变成[1,2]。

正确解法:如果你需要两个独立的列表,必须在开始就创建两个不同的对象。

a = [] b = [] # 不是 b = a! # 或者,如果必须从a派生b a = [1, 2] b = a.copy() # 浅拷贝,适用于纯不可变元素 # b = a[:] # 同上,切片语法 # b = list(a) # 同上,构造函数

排查技巧:在怀疑有共享时,立刻打印id()。print(f"a: {id(a)}, b: {id(b)}")。如果ID相同,那它们就是同一个对象,任何就地修改都会互相影响。这是最快、最准的诊断方法。

5.3 “为什么{}和[]作为函数默认参数,会导致‘记忆’上次调用的结果?”——默认参数的幽灵

现象:这个bug堪称Python入门第一坑。

def bad_append_to_default(item, lst=[]): # ❌ 危险!默认参数是可变对象 lst.append(item) return lst print(bad_append_to_default(1)) # [1] print(bad_append_to_default(2)) # [1, 2] —— 什么?! print(bad_append_to_default(3)) # [1, 2, 3] —— 完全失控!

原因分析:Python函数的默认参数在函数定义时就被求值并创建一次,然后被所有后续调用共享。lst=[]这个空列表对象,在函数第一次被定义时就创建好了,之后每次调用不传lst时,都复用这个对象。所以三次调用,都在往同一个列表里append。

正确解法:永远用None作为可变默认参数的占位符。

def good_append_to_default(item, lst=None): if lst is None: lst = [] # ✅ 每次调用都创建新列表 lst.append(item) return lst print(good_append_to_default(1)) # [1] print(good_append_to_default(2)) # [2] —— 正常! print(good_append_to_default(3)) # [3] —— 正常!

排查技巧:任何包含=的函数定义,如果右边是[],{},set()等可变对象,立刻标红!这是静态代码检查工具(如pylint,flake8)的必检项。在代码审查时,把它当作一个硬性红线。

5.4 “为什么a = [1,2,3]; b = a; del a; print(b)还能打印?”——引用计数的延迟释放

现象:删除了一个变量,但另一个指向同一对象的变量依然能用,对象似乎没被销毁。

原因分析:del a只是删除了命名空间中的一个键,即移除了a这个标签。对象本身的引用计数减1。只要还有其他标签(如b)指向它,它的引用计数就不为0,因此不会被销毁。del不是free(),它不直接释放内存,只是减少一个引用。

延伸思考:如果del a后,b也del了,或者b被重新赋值,那么当引用计数降到0时,对象会被立即销毁(CPython中),并调用其__del__方法(如果定义了的话)。

class TestObj: def __del__(self): print("对象被销毁了!") t = TestObj() print("创建对象") del t # 输出:对象被销毁了!

排查技巧:del语句极少在日常代码中显式使用。它的主要用途是在交互式环境(IPython/Jupyter)中清理大变量,或在循环中显式释放不再需要的大对象(如大型NumPy数组),以帮助GC更快回收。在生产代码中,更推荐让变量自然离开作用域(scope),由Python自动管理。

5.5 “为什么a = 1000; b = 1000; a is b是False,但a = 100; b = 100; a is b却是True?”——小整数缓存的幻觉

现象:is的行为看起来很随机,有时True,有时False,让人困惑。

原因分析:这是CPython的一个实现优化,而非语言规范。CPython为了节省内存和提高效率,对小整数(范围通常是-5到256)进行了缓存。所有在这个范围内的整数,都复用同一个对象。所以a = 100; b = 100,它们都指向缓存池里的那个100对象,id相同。但1000超出了缓存范围,每次1000字面量都会创建一个新对象。

# 验证缓存范围 print(256 is 256) # True print(257 is 257) # False(在标准CPython中) # 但注意:这在交互式环境中可能因编译器优化而不同

正确态度:永远不要用is来比较数字或字符串的值!is只应用于单例(None,True,False)或明确需要身份比较的场景(如if obj is SENTINEL)。比较值,一律用==。

排查技巧:如果你在代码中看到了x is 5或y is "hello",立刻重构为x == 5或y == "hello"。这是一个危险信号,表明作者混淆了身份和值的概念。用pylint的singleton-comparison规则可以自动检测。

6. 工具与调试:让引用关系“看得见”

6.1objgraph:可视化对象引用图的神器

当面对复杂的对象关系网(比如Django ORM的QuerySet、Flask的上下文栈),id()和print已经不够用了。objgraph库能生成直观的引用图。

安装与基础用法:

pip install objgraph
import objgraph # 创建一些对象 a = [1, 2, 3] b = {"key": a} c = (b, "another") # 查看谁引用了对象a objgraph.show_backrefs([a], max_depth=3, filename='backrefs.png') # 生成图片,显示所有指向a的路径 # 查看a被哪些对象引用 objgraph.show_refs([
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 5:36:01

DLSS Swapper 完整教程:一键安装,替换与回滚游戏 DLSS 版本

DLSS Swapper 完整教程:一键安装,替换与回滚游戏 DLSS 版本 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper DLSS Swapper 是一款面向 Windows 玩家的画质增强 DLL 管理工具,可以下载…

作者头像 李华
网站建设 2026/10/16 5:35:17

二级WPS表格基础全解析:工作簿与单元格高频考点指南

考二级WPS的朋友,尤其是自学备考的同学,往往会有一种错觉:表格基础嘛,不就是新建个表格、敲几个字、调调行高列宽,有什么好学的?可等你真正进了考场,打开操作题界面,对着“考生文件夹…

作者头像 李华
网站建设 2026/10/16 5:35:06

ReID损失函数原理与工业级实现:从Triplet到Circle Loss

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华