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 # 运行时