zoukankan      html  css  js  c++  java
  • 『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。

  • 相关阅读:
    C# 字典类 Dictionary 基本用法 Mark
    SQL语句监测耗时
    jQuery Select Option 操作 删除新增
    C# DataTable 过滤重复数据
    IE8 overflow:hidden 无效问题解决方案
    动态拼接LINQ 查询条件
    解决.net中"未能创建 Mutex”异常
    创建Cookies 包含子健和无子健的创建及用法 做个笔记留着参考
    常用的一些加密算法,留着以备不时之需
    Centos7 nginx安装
  • 原文地址:https://www.cnblogs.com/hellcat/p/10450785.html
Copyright © 2011-2022 走看看