Python C 擴展的引用計數問題探討

2021-01-09 阿里云云棲號

Python GC機制

對於Python這種高級語言來說,開發者不需要自己管理和維護內存。Python採用了引用計數機制為主,標記-清除和分代收集兩種機制為輔的垃圾回收機制。

首先,需要搞清楚變量和對象的關係:

變量:通過變量指針引用對象。變量指針指向具體對象的內存空間,取對象的值。對象,類型已知,每個對象都包含一個頭部信息(頭部信息:類型標識符和引用計數器)

引用計數

python裡每一個東西都是對象,它們的核心就是一個結構體:PyObject,其中ob_refcnt就是引用計數。當一個對象有新的引用時,ob_refcnt就會增加,當引用它的對象被刪除,ob_refcnt就會減少。當引用計數為0時,該對象生命就結束了。

typedef struct_object { int ob_refcnt; struct_typeobject *ob_type;} PyObject;#define Py_INCREF(op) ((op)->ob_refcnt++) //增加計數#define Py_DECREF(op) \ //減少計數 if (--(op)->ob_refcnt != 0) \ ; \ else \ __Py_Dealloc((PyObject *)(op))可以使用sys.getrefcount()函數獲取對象的引用計數,需要注意的是,使用時會比預期的引用次數多1,原因是調用時會針對於查詢的對象自動產生一個臨時引用。

下面簡單展現一下引用計數的變化過程。

一開始創建3個對象,引用計數分別是1。之後將n1指向了新的對象"JKL",則之前的對象「ABC」的引用計數就變成0了。這時候,Python的垃圾回收器開始工作,將「ABC」釋放。接著,讓n2引用n1。「DEF」不再被引用,「JKL」因為被n1、n2同時引用,所以引用計數變成了2。

>>> n1 = "ABC">>> n2 = "DEF">>> n3 = "GHI">>> sys.getrefcount(n1)2>>> sys.getrefcount(n2)2>>> sys.getrefcount(n3)2>>> n1 = "JKL">>> sys.getrefcount(n1)2>>> n2 = n1>>> sys.getrefcount(n1)3>>> sys.getrefcount(n2)3>>> sys.getrefcount(n3)2優缺點:

優點:實時性好。一旦沒有引用,內存就直接釋放了。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。

缺點:維護引用計數消耗資源;循環引用無法解決。

如下圖,典型的循環引用場景。對象除了被變量引用n1、n2外,還被對方的prev或next指針引用,造成了引用計數為2。之後n1、n2設成null之後,引用計數仍然為1,導致對象無法被回收。

標記-清除、分代收集

Python採用標記-清除策略來解決循環引用的問題。但是該機制會導致應用程式卡住,為了減少程序暫停的時間,又通過「分代回收」(Generational Collection)以空間換時間的方法提高垃圾回收效率。詳見Python垃圾回收機制!非常實用

Python C擴展的引用計數

Python提供了GC機制,保證對象不被使用的時候會被釋放掉,開發者不需要過多關心內存管理的問題。但是當使用C擴展的時候,就不這麼簡單了,必須需要理解CPython的引用計數。

當使用C擴展使用Python時,引用計數會隨著PyObjects的創建自動加1,但是當釋放該PyObjects的時候,我們需要顯示的將PyObjects的引用計數減1,否則會出現內存洩漏。

#include "Python.h"void print_hello_world(void) { PyObject *pObj = NULL; pObj = PyBytes_FromString("Hello world\n"); /* Object creation, ref count = 1. */ PyObject_Print(pLast, stdout, 0); Py_DECREF(pObj); /* ref count becomes 0, object deallocated. * Miss this step and you have a memory leak. */}有亮點尤其需要注意:

PyObjects引用計數為0後,不能再訪問。類似於C語言free後,不能再訪問對象。Py_INCREF、Py_DECREF必須成對出現。類似於C語言malloc、free的關係。Python有三種引用形式,分別為 「New」, 「Stolen」 和「Borrowed」 引用。

New引用

通過Python C Api創建出的PyObject,調用者對該PyObject具有完全的所有權。一般Python文檔這樣體現:

PyObject* PyList_New(int len) Return value: New reference. Returns a new list of length len on success, or NULL on failure.針對於New引用的PyObject,有如下兩種選擇。否則,就會出現內存洩漏。

使用完成後,調用Py_DECREF將其釋放掉。void MyCode(arguments) { PyObject *pyo; ... pyo = Py_Something(args); ... Py_DECREF(pyo);}將引用通過函數返回值等形式傳遞給上層調用函數,但是接收者必須負責最終的Py_DECREF調用。將引用通過函數返回值等形式傳遞給上層調用函數,但是接收者必須負責最終的Py_DECREF調用。void MyCode(arguments) { PyObject *pyo; ... pyo = Py_Something(args); ... return pyo;}使用樣例:

static PyObject *subtract_long(long a, long b) { PyObject *pA, *pB, *r; pA = PyLong_FromLong(a); /* pA: New reference. */ pB = PyLong_FromLong(b); /* pB: New reference. */ r = PyNumber_Subtract(pA, pB); /* r: New reference. */ Py_DECREF(pA); /* My responsibility to decref. */ Py_DECREF(pB); /* My responsibility to decref. */ return r; /* Callers responsibility to decref. */}// 錯誤的例子,a、b兩個PyObject洩漏。r = PyNumber_Subtract(PyLong_FromLong(a), PyLong_FromLong(b));Stolen引用

當創建的PyObject傳遞給其他的容器,例如PyTuple_SetItem、PyList_SetItem。

static PyObject *make_tuple(void) { PyObject *r; PyObject *v; r = PyTuple_New(3); /* New reference. */ v = PyLong_FromLong(1L); /* New reference. */ /* PyTuple_SetItem "steals" the new reference v. */ PyTuple_SetItem(r, 0, v); /* This is fine. */ v = PyLong_FromLong(2L); PyTuple_SetItem(r, 1, v); /* More common pattern. */ PyTuple_SetItem(r, 2, PyUnicode_FromString("three")); return r; /* Callers responsibility to decref. */}但是,需要注意PyDict_SetItem內部會引用計數加一。

Borrowed引用

Python文檔中,Borrowed引用的體現:

PyObject* PyTuple_GetItem(PyObject *p, Py_ssize_t pos) Return value: Borrowed reference.Borrowed 引用的所有者不應該調用 Py_DECREF(),使用Borrowed 引用在函數退出時不會出現內存洩露。。但是不要讓一個對象處理未保護的狀態Borrowed 引用,如果對象處理未保護狀態,它隨時可能會被銷毀。

例如:從一個 list 獲取對象,繼續操作它,但並不遞增它的引用。PyList_GetItem 會返回一個 borrowed reference ,所以 item 處於未保護狀態。一些其他的操作可能會從 list 中將這個對象刪除(遞減它的引用計數,或者釋放它),導致 item 成為一個懸垂指針。

bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); /* BUG! */}no_bug(PyObject *list) { PyObject *item = PyList_GetItem(list, 0); Py_INCREF(item); /* Protect item. */ PyList_SetItem(list, 1, PyInt_FromLong(0L)); PyObject_Print(item, stdout, 0); Py_DECREF(item);}本文為阿里雲原創內容,未經允許不得轉載。

相關焦點

  • 《小灰教你零基礎學python》-Python入門語言
    程式語言有很多,咱們就學簡單強大的python即可。Python目前是分成2個大版本,python2 和python3,python是完全免費的,所以不用擔心版權問題,因為python2已經廢棄,所以咱們這套課程完全基於python3。
  • 單片機C語言程序設計:INT0 中斷計數
    /*  名稱:INT0 中斷計數說明:每次按下計數鍵時觸發INT0 中斷,中斷程序累加計數,計數值顯示在 3 只數碼管上,按下清零鍵時數碼管清零*/#include#define uchar unsigned char#
  • 旋轉編碼器的抗抖動計數電路
    本文引用地址:http://www.eepw.com.cn/article/255599.htm 關鍵詞:旋轉編碼器 抗抖動電路 數字電路
  • 應用BioPython解析和可視化蛋白質的結構
    BioPython模塊的安裝首先,在終端下安裝BioPython模塊:pip install biopython經過一些提示BioPython即刻安裝完成。這裡多說一句,由於國內應用pip安裝模塊很慢,如果想要高速安裝,可以選用清華大學的鏡像,即清華大學的tuna鏡像。
  • opencv-python獲取圖像:面向對象與面向過程
    運行環境:win10系統 python==3.6 opencv-contrib-python== 4.1.0第一行「# -*- coding: utf-8 -*-」 告訴Python解釋器,按照UTF-8編碼讀取原始碼"""import cv2image=cv2.imread('lena.JPG') #讀取本地圖片,
  • 乾貨| 完美Python入門基礎知識點總結
    Python賦值運算符以下假設變量a為10,變量b為20:=簡單的賦值運算符c = a + b 將 a + b 的運算結果賦值為 c+=加法賦值運算符c += a 等效於 c = c + a-=減法賦值運算符c -= a 等效於 c = c - a*=乘法賦值運算符c *=
  • Python編程常見問題與解答
    5.問:使用pip安裝擴展庫總是提示網絡超時,該怎麼辦呢?答:可以下載安裝包或whl文件離線安裝,或者指定國內源,不使用pip默認的國外源。6.問:使用pip安裝擴展庫時失敗,提示需要安裝VC++,該怎麼辦呢?
  • python基礎教程之python是什麼?
    本文引用地址:http://www.eepw.com.cn/article/265927.htm現在,全世界差不多有600多種程式語言,但流行的程式語言也就那麼20來種。如果你聽說過TIOBE排行榜,你就能知道程式語言的大致流行程度。
  • 開發三年轉測試,除了Python基礎外,還需哪些必備測試技能?
    Q3:python字典和json字符串相互轉化方法是?Python一個高效語言的原因之一是因為它有非常完善的內存管理機制,在python中萬物皆對象,所以圍繞對象的內存管理是整個的核心。第一:對象的引用計數機制,python是使用引用計數來保持追蹤內存中的對象。第二:垃圾回收機制,當一個對象的引用計數歸零時,它將被垃圾收集機制處理掉,保證對象可回收。
  • Python數據類型之列表list
    # 列表是python中最基本的數據結構,它是一個有序序列,序列中的每個元素都分配一個數字(位置,索引)# 1、我們可以使用 方括號,中括號[]來創建列表# 2、我們可以直接將序列放在list(seq)
  • 三分鐘從入門到精通——Python模塊
    中的模塊:假設您正在使用python解釋器。因此,python有一種方法可以將該函數定義放入文件中並隨時使用。模塊是ModuleType類型的對象。該模塊基本上是一個python文件(擴展名為的文件.py),其中包含定義和語句(例如:類,函數,變量等)。Python中的模塊為我們提供了邏輯上組織代碼的靈活性。我們使用模塊將大型程序分解為小的可管理文件。
  • Python 三十大實踐、建議和技巧
    1、使用 python 3由於官方從2020年1月1日起就停止了對python2.7的更新支持,因此本教程的大部分例子都只能在python 3環境下運行。如果你仍然在使用2.7版本,請先升級到python 3。2、檢查並使用滿足需求的最小python版本你可以在代碼中檢查Python 版本,以確保你的代碼使用者沒有使用不兼容的版本運行腳本。
  • 「單光子計數」是X射線探測器製造商擴展投資的不錯選擇!
    打開APP 「單光子計數」是X射線探測器製造商擴展投資的不錯選擇!文章及其配圖僅供工程師學習之用,如有內容圖片侵權或者其他問題,請聯繫本站作侵刪。 侵權投訴
  • 「python學習手冊-筆記」003.數值類型
    003.數值類型本系列文章是我個人學習《python學習手冊(第五版)》的學習筆記,其中大部分內容為該書的總結和個人理解,小部分內容為相關知識點的擴展。非商業用途轉載請註明作者和出處;商業用途請聯繫本人(gaoyang1019@hotmail.com)獲取許可。
  • python交互式shell-ipython
    直接輸入python就可以進入默認的shell,但是都沒有提示,用起來不是很爽。這時候就可以用上ipython。IPython 是一個 python 的交互式 shell,比默認的python shell 好用得多,支持變量自動補全,自動縮進,支持 bash shell 命令,內置了許多很有用的功能和函數。
  • 超全Anaconda(Python整合包)導修(圖文詳解)
    本篇導修將會探討如何運用Anaconda幫助Python編程。' , 2: 'python'}c = (1,2,3,4,5)d = {1,2,3,4,5}print("the listis" , a)print("thedictionary is" ,b)print("the tupleis" , c)print("the set is" , d)操作符(Operators
  • Python數據類型串講(中)
    #創建列表x_listx_list=[2333,'python',['a','b','c']]print(x_list)以上代碼執行的結果為:除了上面1.2節介紹的序列通用操作外,列表還有其他一些比較常用的操作。2.1 元素的更新2.1.1 元素的修改列表通過直接對元素的索引位置賦新值來修改元素。
  • 人生苦短,我用Python,那麼問題來了,普通人要學python嗎?
    話說在金融和IT行當混跡了多年,python一直是被我隨手拿來當個小工具用用,有時候偷懶用python弄個excel自動化整理工具,有時候拿來上各種網站爬蟲搜集點信息,有時候也會拿來寫點量化小工具。那麼到底什麼是python?
  • 「python opencv視覺零到實戰」八、圖片選區操作
    一、學習目標了解什麼是ROI了解floodFill的使用方法如有錯誤歡迎指出~目錄「python opencv 計算機視覺零基礎實戰」 第一節「python opencv視覺入門到實戰」二、格式與攝像頭「python opencv 視覺入門到實戰」 三、圖像編輯「python opencv視覺入門到實戰
  • 剖析C語言中a=a+++a的無聊問題
    同僚們閒聊,突然就聊到了a+++++a的問題。這種純屬C語言 「二」 級的問題應該是從a+++a引申出來的吧。於是乎兄弟姐妹們開始討論它的運算結果,以及改如何理解。