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

一、问题描述

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

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

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

  1. 对象被创建,例如a=23
  2. 对象被引用,例如b=a
  3. 对象被作为参数,传入到一个函数中,例如func(a)
  4. 对象作为一个元素,存储在容器中,例如list1=[a,a]

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

  1. 对象的别名被显式销毁,例如del a
  2. 对象的别名被赋予新的对象,例如a=24
  3. 一个对象离开它的作用域,例如f函数执行完毕时,func函数中的局部变量(全局变量不会)
  4. 对象所在的容器被销毁,或从容器中删除对象

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

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 (<code object func at 0x0000029849AD5D20, file "<dis>", 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。

posted @ 2019-02-28 15:39  叠加态的猫  阅读(1785)  评论(1编辑  收藏  举报