python的引用和拷贝(一)
写在最前:python的地址是什么?
python由于解释器的存在,因此与C/C++的很大不同便是不直接通过地址操纵内存资源,而是使用引用。在 Python 中,引用可以近似理解为 “对对象内存地址的高层抽象表示”。每个 Python 对象在内存中都有唯一的标识符(可通过 id() 函数获取,类似于 “内存地址的编号”)。当你创建变量并赋值时,变量(引用)会指向该对象的内存位置。
| 1 | a = 10 | 
有了引用的概念后,我们接下来就要谈谈python的三种存储方式:引用赋值、浅拷贝、深拷贝。
python赋值的三种手段
引用赋值
引用赋值就是利用上文提出的引用概念,它的机制是将新变量和老变量指向同一个数组对象(也就是共享内存中的数据缓冲区)如果其中一个发生了变化,自然另一个也会发生变化。引用赋值常用在可变对象的赋值(=)运算中(需要提到的是在python中万物皆为对象,即使类本身也可以视为一种对象,比如为list、自定义的实例化对象obj、以及各种含有可变对象的容器如tuple[list]等可变对象成员),换句话说对可变对象的赋值运算默认就是引用赋值。
| 1 | import numpy as np | 
浅拷贝 (view())
创建新的数据对象,但新对象和原对象共享数据缓冲区(内存中的实际数据)。修改新对象的数据,原对象会同步变化(因为内存共享);但修改原对象的属性则不会影响新对象,比如说改变数组对象的形状(shape),原数组不受影响(因为数组对象本身是新的)。
| 1 | a = np.array([1, 2, 3]) | 
需要注意的是,引用赋值和浅拷贝最大的不同便是浅拷贝一定是创建了一个新的对象,对于浅拷贝来说a与b是不同的两个个体,即使他们共享数据缓存!
深拷贝 (copy())
类似于C++中的深拷贝,创建完全独立的新数组,包括新的数据缓冲区(不共享任何内存)。修改新数组的元素或形状,原数组完全不受影响。
| 1 | a = np.array([1, 2, 3]) | 
举个numpy数组的例子
我们假设把numpy数组拆成两个部分:
- 对象本体:存储数组的「属性信息」(比如 shape、dtype、ndim等,相当于数组的 “配置文件”)
- 数据缓冲区:存储数组的「实际数值」(比如 [1,2,3],相当于数组的 “核心数据”)。
1. = 号:引用赋值(不是拷贝)
- 对象本体:同一个(两个变量指向内存中同一个数组对象,属性完全共享);
- 数据缓冲区:同一个(实际数值完全共享)。
简单说:a = b 后,a 和 b 是「同一个东西」,只是名字不同。修改 a 的任何部分(属性或数据),b 会完全同步变化 —— 因为它们本来就是一个对象。
例子:
| 1 | a = np.array([1,2,3]) | 
2. 浅拷贝(view() / 切片):新对象 + 共享数据
- 对象本体:新的(创建一个全新的数组对象,属性初始和原数组一致,但后续可独立修改);
- 数据缓冲区:同一个(实际数值共享,修改一方的数据,另一方会同步变)。
| 1 | a = np.array([1,2,3]) | 
3. 深拷贝(copy()  or deepcopy()):新对象 + 新数据
- 对象本体:新的(独立属性);
- 数据缓冲区:新的(独立数值)。
完全独立的副本,修改任何一方的属性或数据,都和原数组无关。
| 1 | a = np.array([1,2,3]) | 
TIPS:这里需要注意
list和numpy.array中的copy方法是不一样的
对象类型 copy()是浅拷贝还是深拷贝?对应的 “浅拷贝替代” Python 内置容器(列表、字典) 浅拷贝 无( copy()本身就是浅拷贝)numpy数组深拷贝 view()(numpy的浅拷贝)
跟C++做对比
C++ 中,“拷贝” 的前提是 创建了新的对象实例(新的内存块用于存储对象本身),浅拷贝和深拷贝的区别只在于「对象内部成员的处理方式」:
- 
浅拷贝:创建新对象(新对象有自己的内存空间),但新对象的「指针成员」仍指向原对象的内存(共享内部数据) 1 
 2
 3
 4
 5
 6
 7
 8
 9class MyArray { 
 public:
 int* data; // 指针成员(指向堆内存)
 MyArray(int val) { data = new int(val); }
 MyArray(const MyArray& other) : data(other.data) {} // 浅拷贝:只拷贝指针,不拷贝指针指向的内存
 };
 MyArray a(10);
 MyArray b = a; // 浅拷贝:创建了新对象b,但b.data和a.data指向同一块堆内存
- 
深拷贝:创建新对象,且新对象的「指针成员」指向「新的堆内存」(完全复制内部数据),和原对象无任何共享。 
有了这些概念后我们终于可以开始思考进一步的问题,类以及类的成员函数是怎么存的呢?
复杂的结构:类成员
这个问题我们先要引入引用计数这个概念,这来自于python的内存管理
引用计数
一个对象B引用了对象A,那么B实际上指向了A的数据部分,他们拥有着同样的id(),同时在这个时间段内A的引用计数+1。而一旦B不再引用A后,则A的引用计数-1,当A的引用计数为0时,A的数据内存便会立即释放。这就是python的内存回收机制之一,另一个是**GC**。具体规则如下:
- 当一个对象被新的引用指向时(如赋值、作为参数传递、被容器存储),引用计数 +1;
- 当引用失效时(如变量被删除、离开作用域、容器移除元素),引用计数 -1;
- 当引用计数归 0时,对象被回收。
例如:
| 1 | a = obj() # 创建对象,引用计数=1 | 
注意:Python 的引用计数机制是实时检测的:当对象的引用计数变为 0 时,解释器会立即触发内存回收(逻辑上是 “即时”,底层执行可能有微小调度,但无 “定时器定点释放”)。换句话说,释放时机是 “引用计数为 0 的那一刻”,没有额外的延迟或定时器。
而对于三种赋值方式,显然引用赋值会导致引用计数+1(准确来说是被引用的一方+1,但是引用赋值后A与B本质是一体的,所以简称为引用计数+1)。至于深浅拷贝对引用计数的影响,取决于它们是否创建新对象以及是否引用原对象的内部元素。核心规律是:
- 只要创建新对象,新对象的引用计数初始为 1;
- 只要引用了原对象(或其内部元素),被引用对象的引用计数就会增加。
浅拷贝对引用计数的影响:
浅拷贝会创建新的容器对象(如新列表、新字典),但新容器中的元素仍然是原容器中元素的引用(即共享内部元素)。具体影响:
- 新容器对象:引用计数初始为 1(因为刚创建,被当前变量引用);
- 原容器的内部元素:每个元素的引用计数 +1(因为新容器也引用了它们);
- 原容器对象:引用计数不变(浅拷贝不影响原容器本身的引用)。
比如:
| 1 | import sys | 
那么这里print(sys.getrefcount(a[2]))是多少呢?当然,答案是4
深拷贝的影响
深拷贝会创建完全独立的新对象,包括所有嵌套的内部元素都会被复制(即新容器和内部元素都是全新的,不引用原对象的任何部分)。具体影响:
- 新容器对象:引用计数初始为 1;
- 新容器的内部元素:都是全新的副本,引用计数初始为 1(仅被新容器引用);
- 原对象(容器及内部元素):引用计数完全不变(深拷贝不引用原对象的任何部分)。
例如:
| 1 | import sys | 
类成员该怎么存?
实例的属性(包括 “数据属性”)都存储在 __dict__ 中,而引用计数管理的是「整个实例对象的生命周期」(而非单独的 “数据部分”) —— 拷贝(浅 / 深)对引用计数的影响,本质是对「实例对象本身」和「__dict__ 中属性值(可能是对象引用)」的引用计数影响。
__dict__
类的成员可以分成类自身的静态成员以及实例化成员(动态成员)。对实例化成员而言,假设你有一个自定义类实例 A(A=Obj(...)),其 “成员”(属性)无论是 “数据部分”(如 A.data)还是其他属性(如 A.generation),只要是通过 self.xxx = ... 定义的实例属性,都会以「键值对」的形式存储在 A.__dict__ 中,而它的静态成员(比如A中静态字段)则存储在**Obj.__dict__**。
| 1 | class MyObj: | 
可见A 的所有实例属性(包括 “数据部分”data 和 “普通属性”generation)都在 __dict__ 里,__dict__ 本身是一个字典,存储的是「属性名 → 属性值(可能是对象引用)」的映射。当你拷贝 A 得到 B 时,浅拷贝和深拷贝对 __dict__ 及引用计数的影响不同,但核心都是:引用计数影响的是「被引用的对象」(包括实例 A/B 本身,以及 __dict__ 中的属性值对象),而非 “数据部分” 或 “属性部分” 的割裂影响。
1. 浅拷贝 B(如 B = copy.copy(A))
- 
B.__dict__的特点:创建一个「新的__dict__字典」,但字典中的「属性值是原A.__dict__中属性值的引用」(共享属性值对象)。即: B.__dict__["data"]和A.__dict__["data"]指向同一个numpy数组(数据对象)B.__dict__["generation"]和A.__dict__["generation"]指向同一个整数0。
- 
引用计数的影响: - 实例 A本身的引用计数:不变(浅拷贝不影响原实例的引用);
- 实例 B本身的引用计数:初始为1(刚创建,被变量B引用);
- __dict__中属性值对象的引用计数:- 若属性值是「复杂对象」(如 data是numpy数组):引用计数+1(因为B.__dict__也引用了它);
- 若属性值是「基础类型」(如 generation是整数):引用计数也会+1(但基础类型有 “小整数池” 优化,实际计数变化可能被忽略,逻辑上仍遵循规则)。
 
- 若属性值是「复杂对象」(如 
 
- 实例 
2. 深拷贝 B(如 B = copy.deepcopy(A))
- B.__dict__的特点:创建一个「新的- __dict__字典」,且字典中的「属性值是原属性值的独立副本」(不共享任何对象引用)。即:- B.__dict__["data"]是- A.__dict__["data"]的深拷贝(新的- numpy数组);- B.__dict__["generation"]是- A.__dict__["generation"]的副本(新的整数- 0)。
- 引用计数的影响:
- 实例 A本身及A.__dict__中所有属性值的引用计数:完全不变(深拷贝不引用原对象的任何部分);
- 实例 B本身的引用计数:初始为1;
- B.__dict__中属性值对象的引用计数:初始为- 1(仅被- B.__dict__引用)
 
- 实例 
最后:一个担忧
对复杂数据结构的内存引用机制有了了解后,应该会自然想到如果相互之间互相引用也许很容易造成**“一些可能的未知的问题”。事实上没错,这会造成循环引用的问题,这也衍生出了强引用&弱引用**的概念,这些就留给下一篇探讨吧。





