Python 内存相关学习记录

文章目录

本文中交替出现 Python 的编译模式和交互模式代码块,为便于区分,带有 >>> 的 Python 代码块为交互模式,其余 Python 代码块为编译模式。

可变对象与不可变对象

可变对象 不可变对象
列表、字典、集合 整型、浮点型、布尔型、字符串、元组

简单来说,可变对象就是指在修改数据时,直接修改原来的数据对象;不可变对象则是创建一个新的对象,并且将变量的引用转移到新创建的对象上。

 1dictnry = {'a': 0, 'b': 1}  # 字典对象为可变对象
 2print(id(dictnry))
 3dictnry['a'] = 1
 4print(id(dictnry))
 5
 6'''
 7output:
 81570839226304
 91570839226304
10'''

下面是一个不可变对象的例子:

 1string = 'Hello, world?'
 2print(id(string))
 3new_string = string.replace('?', '!')  # 因为字符串是不可变对象,因此在修改时需要一个新的变量接受修改后的字符串
 4print(id(new_string))
 5print(string)
 6print(new_string)
 7
 8'''
 9output:
101668896675760
111668896210864
12Hello, world?
13Hello, world!
14'''

深拷贝与浅拷贝

  • 浅拷贝:拷贝对象的引用。当原对象的数据改变时,拷贝的对象也会发生改变。
  • 深拷贝:创建一个新的对象并将原数据存入新的对象。原对象数据改变不影响拷贝对象

在 Python 中,使用 copy() 函数实现浅拷贝,使用 copy.deepcopy() 函数实现深拷贝。

 1# 浅拷贝
 2>>> a = {'a': [1, 2]}
 3>>> b = a.copy()
 4>>> a, b
 5({'a': [1, 2]}, {'a': [1, 2]})
 6>>> a['a'].append(3)
 7>>> a, b
 8({'a': [1, 2, 3]}, {'a': [1, 2, 3]})
 9# 在浅拷贝中,只拷贝原对象的引用(地址)而非创建一个新的对象,因此原对象的值改变,拷贝的对象也随之改变。
10
11# 深拷贝
12>>> import copy
13>>> a = {'a': [1, 2]}
14>>> b = copy.deepcopy(a)
15>>> a, b
16({'a': [1, 2]}, {'a': [1, 2]})
17>>> a['a'].append(3)
18>>> a, b
19({'a': [1, 2, 3]}, {'a': [1, 2]})
20# 在深拷贝中,拷贝创建了一个新的对象,因此原对象的值发生改变不影响拷贝的对象的值。

打开 Python Shell,尝试以下操作:

 1>>> a = 10
 2>>> b = 10
 3>>> a is b
 4True
 5>>> a = -4
 6>>> b = -4
 7>>> a is b
 8True
 9>>> a = 1234
10>>> b = 1234
11>>> a is b
12False
13>>> a = 'hi!'
14>>> b = 'hi!'
15>>> a is b
16False
17>>> a = 'hello_world'
18>>> b = 'hello_world'
19>>> a is b
20True

注意:Python 中 == 运算符用于判断两个数据在数值上是否相等,而 is 关键字用于判断两个数据的引用是否一致,通俗来说就是地址是否一致。

1>>> a = 10
2>>> b = 10.0
3>>> a == b
4True
5>>> a is b
6False

小整数池

Python 为了节约运行内存,添加了小整数池机制。Python 会将 -5 到 256 (含 -5 与 256)的数据存储在固定位置,在需要时直接引用,而不是创建一个新的整形对象。因此:

1>>> a = 10
2>>> b = 10
3>>> id(a)
4140720040721112
5>>> id(b)
6140720040721112

除了整型对象,仅有的两个布尔型对象也有固定的地址,每次引用都是相同的 True(或者 False)。然而,对于超出小整数池的数据,在引用时就会创建出一个新的对象:

1>>> a = 1234
2>>> b = 1234
3>>> id(a)
42180555911728
5>>> id(b)
62180558782128

当然,上面的操作都在 Python Shell 中完成。如果在 IDLE 中编写好代码再运行,结果也许会有些许不同……

Interning

对于上面的例子,在 IDLE 中写好代码运行的结果如下:

 1a = 1234
 2b = 1234
 3print(id(a))
 4print(id(b))
 5print(a is b)
 6
 7'''
 8output:
 92230861897584
102230861897584
11True
12'''

不难发现,这一次运行程序,两个 1234 的引用一致。这是因为,Python 在创建了一个整型对象 1234 并被变量 a 引用后,变量 b 也需要引用一个整型对象 1234,因此 b 也引用了刚才创建的整型对象。

不仅仅是整型对象,除元组外的不可变对象在编译模式下都适用 Interning 机制,而仅当元组内的所有元素都为不可变对象时,元组才适用 Interning 机制。

字符串驻留

上述 Interning 机制是对于编译模式来讲的,而在交互模式中,同样有字符串驻留机制用于节约内存,以下情况会在交互模式中触发:

  • 字符串的长度为 0 或 1
  • 字符串长度大于 1 且字符串中仅含有字母、数字及下划线
  • 驻留发生在编译时而非运行时

在查找资料时,许多文章都提到由乘法得到的字符串长度小于等于 20 且仅含有字母、数字及下划线时也会触发驻留机制,然而依据我在 Python 3.12 中的尝试:

1>>> a = '1234567890qwertyuiop_ASDFGHJKL' * 10
2>>> b = '1234567890qwertyuiop_ASDFGHJKL' * 10
3>>> len(a)
4300
5>>> len(b)
6300
7>>> a is b
8True

这一条似乎并不成立。

这里面最难以理解的应该是第三条,可以用下面的例子解释:

1>>> a = 'Hi_'
2>>> b = 'Hi' + '_'
3>>> a is b
4True  # 编译时
5>>> a = 'Hi_'
6>>> b = 'Hi'
7>>> b + '_' is a
8False  # 运行时