『Python』爲何調用函數會令引用計數+2

1、問題描述

Python中的垃圾回收是以引用計數爲主,分代收集爲輔,引用計數的缺陷是循環引用的問題。在Python中,若是一個對象的引用數爲0,Python虛擬機就會回收這個對象的內存。python

sys.getrefcount(a)能夠查看a對象的引用計數,可是比正常計數大1,由於調用函數的時候傳入a,這會讓a的引用計數+1函數

致使引用計數+1的狀況:測試

  1. 對象被建立,例如a=23
  2. 對象被引用,例如b=a
  3. 對象被做爲參數,傳入到一個函數中,例如func(a)
  4. 對象做爲一個元素,存儲在容器中,例如list1=[a,a]

致使引用計數-1的狀況:spa

  1. 對象的別名被顯式銷燬,例如del a
  2. 對象的別名被賦予新的對象,例如a=24
  3. 一個對象離開它的做用域,例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
  4. 對象所在的容器被銷燬,或從容器中刪除對象

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

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

咱們對函數進行修改:blog

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 的時候引用計數差值並無增長,因此這個解釋是不對的,實際上另外一個引用是函數棧保存了入參對形參的引用(知乎找到的解釋)。get

2、代碼分析

看到了知乎的解釋,我決定自行驗證一下,測試代碼以下:

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。

相關文章
相關標籤/搜索