python 引用计数 循环引用_『Python』为什么调用函数会令引用计数+2

一、问题描述

Python中的垃圾回收是以引用计数为主,分代收集为辅,引用计数的缺陷是循环引用的问题。在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存。

sys.getrefcount(a)可以查看a对象的引用计数,但是比正常计数大1,因为调用函数的时候传入a,这会让a的引用计数+1

导致引用计数+1的情况:

对象被创建,例如a=23

对象被引用,例如b=a

对象被作为参数,传入到一个函数中,例如func(a)

对象作为一个元素,存储在容器中,例如list1=[a,a]

导致引用计数-1的情况:

对象的别名被显式销毁,例如del a

对象的别名被赋予新的对象,例如a=24

一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)

对象所在的容器被销毁,或从容器中删除对象

在网上看到一段有意思的例子:

import sys

def func(c):

print ('in func function', sys.getrefcount(c) - 1)

print (id(func.__globals__['a']))

print ('init', sys.getrefcount(11) - 1)

a = 11

# print (id(a))

print ('after a=11', sys.getrefcount(11) - 1)

b = a

print ('after b=a', sys.getrefcount(11) - 1)

func(11)

print ('after func(a)', sys.getrefcount(11) - 1)

list1 = [a, 12, 14]

print ('after list1=[a,12,14]', sys.getrefcount(11) - 1)

a=12

print ('after a=12', sys.getrefcount(11) - 1)

del a

print ('after del a', sys.getrefcount(11) - 1)

del b

print ('after del b', sys.getrefcount(11) - 1)

# list1.pop(0)

# print 'after pop list1',sys.getrefcount(11)-1

del list1

print ('after del list1', sys.getrefcount(11) - 1)

输出的init不一定一致,作为计数基础即可(小数int 在python中会默认维护,因为python很多内置量都是小数int,即计数不可能为0),输出中有一点比较奇怪:在传入函数中后计数增加为2,而非设想的1,这是为什么?

init 153

after a=11 154

after b=a 155

in func function 157

after func(a) 155

after list1=[a,12,14] 158

after a=12 155

after del a 155

after del b 154

after del list1 153

我们对函数进行修改:

def func(c):

print ('in func function', sys.getrefcount(c) - 1)

# print (id(func.__globals__['a']))

for attr in dir(func):

print (attr, getattr(func, attr))

替换掉之前的函数,运行之可以发现func.__globals__属性中记录了全局变量键值对 {'a': 11} 这样(以及其他信息),这就是额外的计数来历:局部变量和全局变量的值是相同的,这导致计数+2。

我们知道,函数也是对象,即使不在函数体内我们也可以调用函数的属性、方法,我们把下面一句从函数体中拿出来单独运行,就发现,由于脱离了函数作用域,函数的__globals__属性中对于全局变量的记载('a'、'b')都不见了,这可以理解,脱离了作用域,局部变量和全局变量都失去了意义(两者都是针对某个作用域的概念)。

for attr in dir(func):

print (attr, getattr(func, attr))

测试发现__globals__中记录的{'a': 11}和函数体外的变量 a 是同一个对象(id相同),且在外面增加 b 的时候引用计数差值并没有增加,所以这个解释是不对的,实际上另一个引用是函数栈保存了入参对形参的引用(知乎找到的解释)。

二、代码分析

看到了知乎的解释,我决定自行验证一下,测试代码如下:

import sys

def func(c):

print ('in func function', sys.getrefcount(c)-1)

print ('init', sys.getrefcount(11) - 1)

func(11)

print ('init', sys.getrefcount(11) - 1)

init 106

in func function 108

init 106

进一步分析一下:

from dis import dis

order = \

"""

def func(c):

print ('in func function', sys.getrefcount(c)-1)

print ('init', sys.getrefcount(11) - 1)

func(11)

print ('init', sys.getrefcount(11) - 1)

"""

dis(order)

返回值如下,

2 0 LOAD_CONST 0 (", line 2>)

2 LOAD_CONST 1 ('func')

4 MAKE_FUNCTION 0

6 STORE_NAME 0 (func)

5 8 LOAD_NAME 1 (print)

10 LOAD_CONST 2 ('init')

12 LOAD_NAME 2 (sys)

14 LOAD_ATTR 3 (getrefcount)

16 LOAD_CONST 3 (11)

18 CALL_FUNCTION 1

20 LOAD_CONST 4 (1)

22 BINARY_SUBTRACT

24 CALL_FUNCTION 2

26 POP_TOP

6 28 LOAD_NAME 0 (func)

30 LOAD_CONST 3 (11)

32 CALL_FUNCTION 1

34 POP_TOP

7 36 LOAD_NAME 1 (print)

38 LOAD_CONST 2 ('init')

40 LOAD_NAME 2 (sys)

42 LOAD_ATTR 3 (getrefcount)

44 LOAD_CONST 3 (11)

46 CALL_FUNCTION 1

48 LOAD_CONST 4 (1)

50 BINARY_SUBTRACT

52 CALL_FUNCTION 2

54 POP_TOP

56 LOAD_CONST 5 (None)

58 RETURN_VALUE

着重看6:

6 28 LOAD_NAME 0 (func)

30 LOAD_CONST 3 (11)

32 CALL_FUNCTION 1

34 POP_TOP

这里将函数 func 和常量11压入了函数栈,会导致引用计数 +1。

我们再看下面代码:

dis(func)

返回的是 func 函数内部操作:

4 0 LOAD_GLOBAL 0 (print)

2 LOAD_CONST 1 ('in func function')

4 LOAD_GLOBAL 1 (sys)

6 LOAD_ATTR 2 (getrefcount)

8 LOAD_FAST 0 (c)

10 CALL_FUNCTION 1

12 LOAD_CONST 2 (1)

14 BINARY_SUBTRACT

16 CALL_FUNCTION 2

18 POP_TOP

20 LOAD_CONST 0 (None)

22 RETURN_VALUE

这里会读取变量 c(偏移量8的操作码),最终导致了增加计数为 2。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值