一、問題描述
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,這是為什么?
我們對函數進行修改:
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 函數內部操作:
這里會讀取變量 c(偏移量8的操作碼),最終導致了增加計數為 2。
