面试题答案
一键面试按值调用(Call by Value)
在Python中,严格来说没有传统意义上的按值调用。Python中的变量是对象的引用,在函数调用时,传递的是对象引用的副本,这种方式类似按值调用。
- 内存分配:
- 函数调用时,实参对象的引用被复制到形参中。例如:
def func(x):
pass
a = 5
func(a)
这里a
是一个指向整数对象5
的引用,调用func(a)
时,x
得到了a
引用的副本,指向同一个整数对象5
。整数对象5
在内存中只有一份。
- 内存使用:
- 函数内部对形参的操作,不会改变实参引用所指向的对象。例如:
def func(x):
x = 10
a = 5
func(a)
print(a) # 输出5
函数内部x = 10
,x
重新指向了一个新的整数对象10
,但a
仍然指向原来的对象5
。
- 内存释放:
- 当函数返回时,形参(即引用副本)会被销毁。如果函数内部没有创建额外的循环引用等情况,对象的引用计数会相应减少。例如上述例子中,函数返回后,
x
这个引用副本被销毁,整数对象5
的引用计数减1。当对象的引用计数降为0时,Python的垃圾回收机制会立即回收该对象的内存。
- 当函数返回时,形参(即引用副本)会被销毁。如果函数内部没有创建额外的循环引用等情况,对象的引用计数会相应减少。例如上述例子中,函数返回后,
按引用调用(Call by Reference)
Python中没有传统按引用调用的概念,但对于可变对象(如列表、字典等),函数内部对其修改会影响到外部对象,这类似于按引用调用的效果。
- 内存分配:
- 同样是传递对象引用的副本。例如:
def func(lst):
pass
my_list = [1, 2, 3]
func(my_list)
my_list
是指向列表对象[1, 2, 3]
的引用,调用func(my_list)
时,lst
得到了my_list
引用的副本,指向同一个列表对象。
- 内存使用:
- 对于可变对象,函数内部可以修改对象本身。例如:
def func(lst):
lst.append(4)
my_list = [1, 2, 3]
func(my_list)
print(my_list) # 输出[1, 2, 3, 4]
这里函数内部对lst
(即my_list
引用副本指向的列表对象)进行了修改,my_list
指向的列表对象也发生了变化。
- 内存释放:
- 当函数返回时,形参引用副本被销毁。如果没有其他地方引用该可变对象,其引用计数降为0,垃圾回收机制回收内存。但如果存在循环引用,情况会更复杂。
循环引用与垃圾回收机制
- 循环引用示例:
class A:
def __init__(self):
self.b = None
class B:
def __init__(self):
self.a = None
a = A()
b = B()
a.b = b
b.a = a
这里a
和b
对象相互引用,形成了循环引用。
- 垃圾回收:
- Python使用引用计数为主,标记 - 清除和分代收集为辅的垃圾回收机制。对于上述循环引用,仅靠引用计数无法回收
a
和b
对象,因为它们相互引用,引用计数不会降为0。 - 标记 - 清除算法会定期扫描堆内存,标记所有可达对象(从根对象开始,如全局变量、栈上的变量等),然后清除未标记的不可达对象,这样就可以回收循环引用的对象。
- 分代收集基于对象存活时间将对象分为不同代,新创建的对象在年轻代,存活时间长的对象会晋升到老年代。垃圾回收器会更频繁地扫描年轻代,提高垃圾回收效率。
- Python使用引用计数为主,标记 - 清除和分代收集为辅的垃圾回收机制。对于上述循环引用,仅靠引用计数无法回收
通过上述分析和代码示例,可以清晰了解Python中类似按值调用和按引用调用方式下内存管理的情况,以及循环引用和垃圾回收机制的作用。