【新人系列】Python 入门(三十一):内存管理

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog

📝 专栏地址:https://blog.csdn.net/newin2020/category_12801353.html

📣 专栏定位:为 0 基础刚入门 Python 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~

📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Python 的学习。在这个 Python 的新人系列专栏下,将会总结 Python 入门基础的一些知识点,方便大家快速入门学习~

❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 内存管理

1.1 对象的创建与存储

对象的创建

  • 当在 Python 中创建一个对象时,例如一个整数、字符串或列表等,Python 会在内存中为这个对象分配空间。
  • 对象的创建可以通过直接赋值、函数调用、类实例化等方式进行。

对象的存储

  • Python 使用一种称为堆(heap)的内存区域来存储对象。堆是一种动态分配的内存空间,可以根据需要进行增长和收缩。
  • 对象在堆中的存储位置是由 Python 的内存管理器自动确定的,程序员通常不需要关心具体的内存地址。

1.2 引用计数

基本概念

  • Python 使用引用计数来跟踪对象的引用数量。当一个对象被创建时,它的引用计数被设置为 1。每当有一个新的引用指向这个对象时,引用计数就会增加;当一个引用被删除时,引用计数就会减少。
  • 当对象的引用计数变为 0 时,说明没有任何引用指向这个对象,此时 Python 的垃圾回收器会自动回收这个对象所占用的内存空间。

引用加 1 常见场景

  • 对象被创建:a = "hello"
  • 对象赋值给另外的对象:b = a
  • 被作为参数传递给函数:func(a)
  • 对象作为一个可变对象或是容器:list = [1, 2, a]

引用减 1 常见场景

  • func(a) 函数结束时,a 指向的对象引用减 1
  • 对象被显示销毁:del a
  • 对象移除的方法:mylist.remove(a)

示例

python 复制代码
a = [1, 2, 3]  # 创建一个列表对象,引用计数为 1
b = a  # 增加一个引用,引用计数变为 2
print(sys.getrefcount(b)) # 2
del a  # 删除一个引用,引用计数变为 1
del b  # 删除另一个引用,引用计数变为 0,此时列表对象可以被垃圾回收

1.3 垃圾回收

如前所述,Python 主要通过引用计数来进行垃圾回收。当对象的引用计数变为 0 时,它会被自动回收。

这种方式简单高效,对于大多数情况都能很好地工作。但是,它也有一些局限性,例如无法处理循环引用的情况。

循环引用

循环引用是指两个或多个对象相互引用,导致它们的引用计数永远不会变为 0。例如:

python 复制代码
class A:
 def __init__(self):
     self.b_ref = None

class B:
 def __init__(self):
     self.a_ref = None

a = A()
b = B()
a.b_ref = b
b.a_ref = a

为了解决这个问题,Python 中有如下的方法:

  1. 使用弱引用
  • Python 中的 weakref 模块提供了弱引用的功能。弱引用不会增加对象的引用计数,当对象的强引用消失时,即使有弱引用存在,对象也会被垃圾回收。
  • 可以使用弱引用来避免循环引用的问题。例如,可以将对象之间的强引用改为弱引用,这样当其他强引用消失时,这些对象可以被正确地回收。

以下是一个使用弱引用解决循环引用的示例:

python 复制代码
import weakref

class A:
 def __init__(self):
     self.b_ref = None

class B:
 def __init__(self):
     self.a_ref = None

a = A()
b = B()
a.b_ref = weakref.ref(b)
b.a_ref = weakref.ref(a)

在这个例子中,对象 a 和 b 之间的引用改为了弱引用,当其他强引用消失时,这两个对象可以被正确地回收。

  1. 手动解除引用
  • 在一些情况下,可以手动解除对象之间的引用,以避免循环引用的问题。例如,在对象的生命周期结束时,显式地将对象之间的引用设置为 None。

以下是一个手动解除引用的示例:

python 复制代码
class A:
 def __init__(self):
     self.b_ref = None

class B:
 def __init__(self):
     self.a_ref = None

a = A()
b = B()
a.b_ref = b
b.a_ref = a

# 在适当的时候解除引用
a.b_ref = None
b.a_ref = None

在这个例子中,在适当的时候手动将对象之间的引用设置为 None,这样当其他强引用消失时,这些对象可以被正确地回收。

垃圾回收机制

  1. 标记清除

标记清除算法会定期遍历所有的对象,标记那些可以被访问到的对象,然后回收那些没有被标记的对象。

  • 工作原理:
    • 标记清除算法主要分为两个阶段:标记阶段和清除阶段。
    • 在标记阶段,垃圾回收器从根对象(如全局变量、局部变量、函数参数等)开始,沿着对象的引用关系进行遍历,标记所有可以被访问到的对象。
    • 在清除阶段,垃圾回收器扫描整个内存空间,回收那些没有被标记的对象所占用的内存空间。
  • 处理循环引用:
    • 对于循环引用的情况,标记清除算法可以正确地识别和回收这些对象。因为它是通过遍历对象的引用关系来确定哪些对象是可以被访问到的,而不是仅仅依赖于引用计数。
    • 例如,在前面提到的循环引用的例子中,如果没有其他地方引用对象 a 和 b,那么在标记清除算法中,这两个对象将不会被标记,从而在清除阶段被回收。
  • 触发时机:
    • Python 的垃圾回收器会在一定条件下自动触发标记清除算法。这些条件包括内存分配达到一定阈值、一定时间间隔没有进行垃圾回收等。
    • 此外,程序员也可以通过调用 gc.collect() 函数来手动触发垃圾回收。
  1. 分代回收

分代回收是基于这样一个观察:大多数对象的生命周期都很短,而一些长期存在的对象则很少被回收。Python 将对象分为不同的代(generation),新创建的对象属于年轻代,经过多次垃圾回收仍然存活的对象会被移动到年老代。年轻代的垃圾回收频率较高,而年老代的垃圾回收频率较低。

  • 基本概念:
    • 分代回收是基于这样一个观察:大多数对象的生命周期都很短,而一些长期存在的对象则很少被回收。
    • Python 将对象分为不同的代(generation),通常分为三代:年轻代(young generation)、中年代(middle generation)和老年代(old generation)。新创建的对象属于年轻代,经过多次垃圾回收仍然存活的对象会被移动到中年代,再经过多次垃圾回收仍然存活的对象会被移动到老年代。
  • 回收策略:
    • 年轻代的垃圾回收频率较高,因为年轻代中的对象通常生命周期很短,很快就会变成不可达对象。中年代和老年代的垃圾回收频率较低,因为这些对象通常比较稳定,不太容易变成不可达对象。
    • 对于年轻代的垃圾回收,通常使用一种称为 "复制收集"(copying collection)的算法。这种算法将年轻代分为两个半区,当垃圾回收时,将可以被访问到的对象复制到另一个半区,然后清空原来的半区。这样可以快速回收不可达对象,并且减少内存碎片。
    • 对于中年代和老年代的垃圾回收,通常使用标记清除算法。
  • 优势:
    • 分代回收可以提高垃圾回收的效率。因为对于大多数程序来说,大部分的垃圾回收工作都集中在年轻代,而年轻代的垃圾回收速度很快。同时,对于中年代和老年代的垃圾回收频率较低,可以减少垃圾回收对程序性能的影响。

1.4 内存池

目的与作用

  • Python 为了提高内存分配的效率,使用了内存池(memory pool)的技术。内存池是一种预先分配一定数量的内存块的机制,当需要分配内存时,可以直接从内存池中获取,而不需要每次都向操作系统申请内存。
  • 这样可以减少内存分配的开销,特别是对于频繁创建和销毁的小对象,可以显著提高程序的性能。

对象的缓冲池

  • 对于一些小的整数、字符串等对象,Python 会使用对象缓冲池来避免频繁的创建和销毁。例如,对于小整数,Python 会在启动时预先创建一个整数对象的范围,并在这个范围内重复使用这些对象。
  • 这样可以减少内存的占用和垃圾回收的压力。

2. 面试常考基础题

2.1 如何查看 Python 有哪些内置函数?

  1. 从 Python 官网查看:https://docs.python.org/3/library/functions.html
  2. 通过 print 语句打印
python 复制代码
import __builtins__
print(dir(__builtins__))

需求 1:用一行代码实现 1 到 100 的和

python 复制代码
sum(range(1, 100))

需求 2:给你一个已知的列表,对这个列表进行去重

python 复制代码
my_list = [1, 1, 2, 3, 3, 4, 5, 5, 6, 7]
  • 使用集合 set 进行去重
python 复制代码
print(set(my_list))
  • 通过创建一个空列表 + 循环
python 复制代码
new_list = []
for i in my_list:
    if i not in new_list:
        new_list.append(i)
print(new_list)

2.2 可变对象和不可变对象的区别

Python 中一切皆为对象,对象在内存中会有两个部分(对象的值和对象的地址)。

  • 列表、字典和集合属于可变对象,可变对象是指在创建后可以扩展修改其值或内容的对象。
  • 整形、浮点数、字符串和元组都属于不可变对象,不可变对象是指创建后其值不能被修改的对象。如果要修改不可变对象的值,实际上会创建一个新的对象,并将变量重新指向这个新对象。

我们来看个可变对象的例子,当可变对象作为默认值的时候,可能会有一些不符合预期的结果发生。

python 复制代码
def func(x, l=[]):
    for i in range(x):
        l.append(i)
    print(l)

func(2)     # [0, 1]
func(3, [3, 2, 1])      # [3, 2, 1, 0, 1, 2]
func(3)     # [0, 1, 0, 1, 2]

因此,我们在日常编码时要避免将可变对象作为默认值传入,如果期望每次调用 func 函数时列表 l 都是新的对象,则可以采取下面这种方式。

python 复制代码
def func(x, l=None):
    if l is None:
        l = []
    for i in range(x):
        l.append(i)
    print(l)
    
func(2)     # [0, 1]
func(3, [3, 2, 1])      # [3, 2, 1, 0, 1, 2]
func(3)     # [0, 1, 2]

2.3 深拷贝和浅拷贝的区别

浅拷贝定义

创建一个新的对象,但对于对象中的元素,如果是引用类型(如列表、字典等嵌套的数据结构),则仅仅复制其引用,而不是复制整个对象。

可以使用 copy.copy() 函数或切片操作来实现浅拷贝。例如:

python 复制代码
# 方法一
new_list = copy.copy(old_list)
# 方法二
new_list = old_list[:]
对于嵌套的对象(如包含列表的列表),只复制最外层容器,内部的嵌套对象仍然是共享的引用。例如:
import copy

original_list = [0, [1, 2], [3, 4]]
shallow_copied_list = copy.copy(original_list)

# 修改内层嵌套的元素
original_list[1][0] = 10
print(original_list)  # [0, [10, 2], [3, 4]]
print(shallow_copied_list)  # [0, [10, 2], [3, 4]]

# 修改外层元素
original_list[0] = 5
print(original_list)  # [5, [10, 2], [3, 4]]
print(shallow_copied_list)  # [0, [10, 2], [3, 4]]

上面修改了原始列表中的嵌套列表的元素,浅拷贝的列表也受到了影响,因为它们共享了内部嵌套列表的引用。

深拷贝定义

创建一个新的对象,并且对于对象中的元素,如果是引用类型,会递归地复制整个对象结构,而不是仅仅复制引用。

使用 copy.deepcopy() 函数来实现深拷贝。例如:

python 复制代码
new_list = copy.deepcopy(old_list)

对于嵌套的对象,会完全独立地复制整个嵌套结构,修改原始对象不会影响深拷贝得到的对象。例如:

python 复制代码
import copy

original_list = [0, [1, 2], [3, 4]]
shallow_copied_list = copy.deepcopy(original_list)

# 修改内层嵌套的元素
original_list[1][0] = 10
print(original_list)  # [0, [10, 2], [3, 4]]
print(shallow_copied_list)  # [0, [1, 2], [3, 4]]

# 修改外层元素
original_list[0] = 5
print(original_list)  # [5, [10, 2], [3, 4]]
print(shallow_copied_list)  # [0, [1, 2], [3, 4]]

深拷贝后,修改原始列表中的元素不会影响深拷贝得到的列表。

性能与内存使用

  • 浅拷贝:通常比深拷贝更快,因为它只需要复制一层对象结构,而不需要递归地复制所有嵌套对象。
  • 深拷贝:可能会占用更多的内存,因为它需要复制整个对象结构,包括所有嵌套的对象。

应用场景

  • 浅拷贝
    • 当需要创建一个新的对象,但又希望尽可能少地复制数据时。
    • 当确定原始对象和拷贝对象中的引用类型对象不会被修改,或者修改不会相互影响时。
  • 深拷贝
    • 当需要完全独立的对象副本,确保对原始对象或副本的任何修改都不会影响到对方时。
    • 当处理复杂的对象结构,且可能存在对嵌套对象的修改时。

Tips:

浅拷贝和深拷贝只针对可变对象 ,如果对象是不可变对象,那么不管是深拷贝还是浅拷贝,拷贝后都不会生成新的对象。

2.4 下划线 _ 在 Python 中的作用

1. 作为变量名

在 Python 中,有以下几种方式来定义变量:

  • xx:公有变量
  • _xx:单前置下划线,私有化属性或方法,类对象和子类可以访问,from somemodule import * 禁止导入
  • __xx:双前置下划线,私有化属性或方法,无法在外部直接访问
  • xx :双前后下划线,系统定义名字,例如 init
  • xx_:单后置下划线,用于避免与 Python 关键词的冲突

各种变量

  • 临时变量

在交互模式下或某些情况下,可以用单个下划线作为临时变量名,表示一个不需要在后续代码中使用的变量。

python 复制代码
for _ in range(5):
   print("Hello")

这里的 _ 只是作为循环的临时计数器,不需要在循环外部使用。

  • 避免与关键字冲突

如果变量名与 Python 关键字冲突,可以在变量名后添加一个下划线来避免冲突。

python 复制代码
class_ = "MyClass"

这里避免了使用 class 关键字作为变量名。

  • 私有化变量

在 Python 类中,以双下划线开头的变量名(如 __var)会触发名称修饰。这意味着这些变量名会被修改为 _ClassName__var 的形式,从而在一定程度上阻止外部直接访问。

python 复制代码
class MyClass:
   def __init__(self):
       self.__private_var = 10

obj = MyClass()
print(obj._MyClass__private_var)

直接访问 __private_var 会导致错误,但可以通过名称修饰后的名称来访问,不过这不是推荐的做法。

  • 保护变量

在 Python 中没有真正意义上的保护变量,但通常使用单下划线开头的变量名(如 _var)来表示一个受保护的变量。这是一种约定,告诉其他程序员这个变量不应该在外部直接访问,但实际上仍然可以访问。

python 复制代码
class MyClass:
 def __init__(self):
     self._protected_var = 10

obj = MyClass()
print(obj._protected_var)

虽然可以直接访问 _protected_var,但按照约定,应该避免在外部直接访问和修改它。

2. 在循环和推导式中表示不关心的变量

  • 在循环中

当在循环中只关心循环次数而不关心具体的循环变量值时,可以使用下划线作为循环变量名。

python 复制代码
for _ in range(10):
   # 执行一些操作,不需要使用循环变量的值
   pass
  • 在推导式中

类似地,在列表推导式、字典推导式等中,如果某个值不需要在表达式中使用,可以用下划线表示。

python 复制代码
numbers = [1, 2, 3, 4, 5]
squared_numbers = [x**2 for x in numbers if x > 2]
# 如果不需要使用判断条件中的值,可以这样写
squared_numbers = [x**2 for _, x in enumerate(numbers) if x > 2]

3. 国际化和翻译

  • 在国际化库中

在一些国际化和翻译的框架中,下划线通常用于标记需要翻译的字符串。例如,使用 gettext 库时:

python 复制代码
import gettext
_ = gettext.gettext
print(_("Hello, world!"))

这里的 _ 函数用于获取翻译后的字符串。

4. 数字分隔符

  • 在数字中

从 Python 3.6 开始,可以使用下划线作为数字的分隔符,以提高数字的可读性。

python 复制代码
large_number = 1_000_000

这里的 1_000_000 和 1000000 是等价的,但使用下划线分隔后更易于阅读。

2.5 Python 推导式

Python 推导式是一种简洁而强大的语法结构,可以用于快速创建列表、字典和集合等数据结构。

基本语法

  • expression for item in iterable if condition

  • 其中,expression 是对每个 item 进行的操作,iterable 是一个可迭代对象,condition 是一个可选的条件表达式,用于筛选出满足条件的 item。
    示例
  • 创建一个包含 0 到 9 的平方的列表、字典和集合:
python 复制代码
# 列表
squares = [i**2 for i in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
 
# 字典
squares_dict = {i: i**2 for i in range(10)}
print(squares_dict)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

# 集合
numbers_set = {i**2 for i in range(10)}
print(numbers_set)  # {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
  • 筛选出偶数的平方:
python 复制代码
even_squares = [i**2 for i in range(10) if i % 2 == 0]
print(even_squares)  # [0, 4, 16, 36, 64]
  • 创建一个索引偶数为 1,奇数为 0 且数组大小为 5 的列表:
python 复制代码
squares = [1 if i % 2 == 0 else 0 for i in range(5)]    
print(squares)    # [1, 0, 1, 0, 1] 
  • 从字典中筛选出得分大于等于 95 的元素:
python 复制代码
dict = {
    'c': 99,
    'python': 100,
    'c++': 80,
    'java': 95
}
squares_dict = {k: v for k, v in dict.items() if v >= 95}
print(squares_dict)    # {'c': 99, 'python': 100, 'java': 95}
相关推荐
闲人编程22 分钟前
自动化文件管理:分类、重命名和备份
python·microsoft·分类·自动化·备份·重命名·自动化文件分类
沐知全栈开发1 小时前
Python3 集合
开发语言
摇滚侠1 小时前
Spring Boot 3零基础教程,新特性 ProblemDetails,笔记50
spring boot·笔记
一只一只1 小时前
Unity 3D笔记(进阶部分)——《B站阿发你好》
笔记·3d·unity·游戏引擎
Jonathan Star1 小时前
用Python轻松提取视频音频并去除静音片段
开发语言·python·音视频
月临水1 小时前
Git 学习笔记
笔记·git·学习·1024程序员节
Evand J2 小时前
【自适应粒子滤波MATLAB例程】Sage Husa自适应粒子滤波,用于克服初始Q和R不准确的问题,一维非线性滤波。附下载链接
开发语言·matlab·卡尔曼滤波·自适应滤波·非线性
roykingw2 小时前
【思想比实现更重要】高并发场景下如何保证接口幂等性
java·web安全·面试
尘觉2 小时前
面试-浅复制和深复制?怎样实现深复制详细解答
javascript·面试·职场和发展