最近在看Python的性能優化方面的文章,突然想起ctypes這個模塊,對於這個模塊一直不是很理解,不過再次看完相關資料有了些新的觀點。
ctypes 這個模塊個人觀點就是提供一個Python類型與C類型數據轉換接口或者說是規則的一個模塊。ctypes定義的數據類型其實並不是一種數據類型,而更應該說是一種轉換規則。ctypes定義的數據類型都是需要和Python數據類型進行關聯的,然后傳給C函數進行調用,在C函數調用的時候是根據ctypes的數據類型進行轉換的,把關聯的Python數據類型轉換為C數據類型傳給C函數。如果是ctypes定義的指針或者地址,其實是將Python變量對應的內存空間地址中的數據與ctypes數據類型進行關聯,如果C函數內部對傳過來的指針地址對應的變量進行修改,最后是ctypes將修改好的C數據類型轉為Python類型數據並將其存入之前Python變量對應的內存空間中。
在調用ctypes時,程序的內存空間其實可以分為Python數據內存空間與C數據類型空間。ctypes定義的數據類型就是提供了一個Python數據類型與C數據類型轉換的對應關系。ctypes定義的數據類型都是需要和Python數據類型關聯的,在調用C函數的時候在實時的轉為C數據類型。其中,Python數據類型存在與Python數據內存空間中,C數據類型存在與C數據內存空間中。
需要注意的一點是,一般情況下C數據內存空間是實時開辟的,用完就及時自動銷毀的,當然也有特例,那就是numpy定義的array類型變量等, numpy定義的數據類型其實就是一種經過包裝的C數據類型,當然numpy定義的array等類型變量存在於C數據內存空間中,而numpy下的array是可以持續存在的,不會自動銷毀。
ctypes 提供了一些基本數據類型用來映射 C 語言和 Python 的類型, 可以這樣說 ctypes 是提供的一種數據類型映射關系或是轉換關系。
給出ctypes的一些用法代碼:
from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) a = b'a' b = b'b' s1 = c_char_p(a) s2 = c_char_p(b) print(id(a), id(b)) print('-'*80) print( s1.value, type(s1), id(s1), id(s1.value), type(s1.value) ) print( s2.value, type(s2), id(s2), id(s2.value), type(s2.value) ) print('*'*80) clib.strcat.argtype = (c_char_p, c_char_p) clib.strcat.restype = c_char_p s3 = clib.strcat(s1,s2) print( s1.value, type(s1), id(s1), id(s1.value), type(s1.value) ) #ab print( s2.value, type(s2), id(s2), id(s2.value), type(s2.value) ) #b #print(s3, type(s3), id(s3) ) #print(a, id(a)) #print(b, id(b)) print("^"*80) s1.value = b'c' print( s1.value, type(s1), id(s1), id(s1.value), type(s1.value) ) #ab print( s2.value, type(s2), id(s2), id(s2.value), type(s2.value) ) #b
結果:
(140474265252848, 140474265252896)
--------------------------------------------------------------------------------
('a', <class 'ctypes.c_char_p'>, 140474264436032, 140474265252848, <type 'str'>)
('b', <class 'ctypes.c_char_p'>, 140474263869200, 140474265252896, <type 'str'>)
********************************************************************************
('ab', <class 'ctypes.c_char_p'>, 140474264436032, 140474263886368, <type 'str'>)
('b', <class 'ctypes.c_char_p'>, 140474263869200, 140474265252896, <type 'str'>)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
('c', <class 'ctypes.c_char_p'>, 140474264436032, 140474265292896, <type 'str'>)
('b', <class 'ctypes.c_char_p'>, 140474263869200, 140474265252896, <type 'str'>)
用ctypes定義結構體轉換規則:(轉換為C數據內存空間時為一種鏈表結構)
from ctypes import * class Test(Structure): pass Test._fields_ = [('x', c_int), ('y', c_char), ('next', POINTER(Test))] test = Test(11, 97, None) test2 = Test(12, 98, pointer(test)) test3 = Test(13, 99, pointer(test2)) print(test) print(type(test)) print(test.x, test.y, test.next) print(type(test.x), type(test.y), type(test.next)) test2=test3.next.contents print(test2) print(type(test2)) print(test2.x, test2.y, test2.next) print(type(test2.x), type(test2.y), type(test2.next)) print(test3) print(type(test3)) print(test3.x, test3.y, test3.next) print(type(test3.x), type(test3.y), type(test3.next))
Test 類相當於把多個python數據類型與C數據類型映射關系打包在一起,是Structure類的繼承類。而Test類的對象則是關聯好了對應的python數據類型對象,轉換時調用Test類的具體對象(如,test1,test2,test3)便會根據打包的python數據類型在C數據內存空間生成對應映射C類型數據。
需要注意的是不論使用ctypes定義數據類型還是結構體,其實都是不換為對應C數據類型開辟內存空間的,ctypes只是提供兩者之間的對應關系:
如:
a = 100
b=ctypes.c_int(a)
其中,b是表示將根據Python數據類型int的a轉換成C內存空間int的一種關系,當C函數調用b時則自動根據b所定義的規則在C數據內存空間中開辟C語言int類型為100的數據。
給出Python官方給出的 ctypes 使用說明:
https://docs.python.org/zh-cn/3/library/ctypes.html
=================================================================
舉例說明Python變量與Python數據內存空間的關系:
Python 變量a:
a變量------------>對應內存地址空間(13521458792)-------------------------->該空間存儲的Python數據為Python類型的999。
上面的關系是由定義 a=999 生成的, 其中id(a)=13521458792 。
======================================================================
為了更清楚的證明ctypes只是提供映射關系,可以看下面代碼:
import ctypes import random d=ctypes.c_double*100000000 data=[] for i in range(100000000): data.append(random.random()) #上半部分 ################################# #下半部分 c_data=d(*data)
在ide中執行上面代碼(分步執行):
執行完上部分代碼,查看內存使用情況:
執行完下部分代碼,再次查看內存使用情況:
可以看出 ctypes 並沒有提供數據類型的轉換,而是提供了數據轉換時對應的映射關系。
如果cytpes 定義的數據類型會直接生成對應的C類型數據在C類型的內存空間中那么執行下部分代碼后的內存占用應該是 24.2% 以上,而實際只是從 12.1% 提高到了14.5%,多占用2.4%的內存,而這2.4%的內存不可能是轉換后的C類型數據只能是其對應的映射關系。
===============================================================================
參考文章:
https://www.cnblogs.com/night-ride-depart/p/4907613.html
ctypes 中的指針:
==================================================================
本文的前部分說Python程序中C語言類型的數據空間並不會持續存在,會隨着用完自動銷毀,其實這個說法並不准確,如果我們將C數據內存空間下的數據給以一定方式保存(某種數據結構的形式保存在堆或棧中),也是可以進行持續保存的,當然這種保存是只在C類型內存空間中,如果要轉回Python類型空間還是需要再轉換的。
給出一個 C語言代碼:
//test.c #include <stdio.h> #include <stdlib.h> // Point 結構體 struct Point { int x; char y; struct Point *p; }; static struct Point* head = NULL; static struct Point* current = NULL; static struct Point* walk = NULL; void new_point(struct Point *p) { if (p) { if(current==NULL) //是第一個element { head=p; current=p; printf("first add !!! \n"); } else //不是第一個element { current->p=p; current=p; printf("not first add !!! \n"); } // } } void print_point() { walk=head; if(walk==NULL) { printf("error, it is a empty list!!! \n"); } else { while(walk!=current) { printf("x: %d, y: %c \n", walk->x, walk->y); walk=walk->p; } if(walk!=NULL) { printf("x: %d, y: %c \n", walk->x, walk->y); } } } void main() { struct Point a; a.x=97; a.y='a'; a.p=NULL; struct Point b; b.x=98; b.y='b'; b.p=NULL; struct Point c; c.x=99; c.y='c'; c.p=NULL; struct Point d; d.x=100; d.y='d'; d.p=NULL; struct Point e; e.x=101; e.y='e'; e.p=NULL; new_point(&a); new_point(&b); new_point(&c); new_point(&d); new_point(&e); print_point(); }
該文件命名為 test.c 。
在Ubuntu18.04系統中編譯並執行:
gcc test.c
編譯成動態鏈接庫:
gcc -fPIC -shared test.c -o test.so
有了動態鏈接庫,我們就可以使用ctypes模塊將Python數據轉為C類型數據並調用C語言下鏈接庫的函數,給出代碼:
import ctypes from ctypes import c_int, c_char, Structure, POINTER, pointer, cdll class Point(Structure): pass Point._fields_ = [('x', c_int), ('y', c_char), ('next', POINTER(Point))] a=Point(97, b'a', None) b=Point(98, b'b', None) c=Point(99, b'c', None) d=Point(100, b'd', None) e=Point(101, b'e', None) a.next=pointer(b) b.next=pointer(c) c.next=pointer(d) d.next=pointer(e) clib = cdll.LoadLibrary('./test.so') clib.new_point.argtype = POINTER(Point) clib.new_point.restype = None clib.print_point.argtype = None clib.print_point.restype = None clib.new_point(pointer(a)) clib.new_point(pointer(b)) clib.new_point(pointer(c)) clib.new_point(pointer(d)) clib.new_point(pointer(e)) print("-"*50) clib.print_point() print("-"*50) clib.print_point()
該Python代碼命名為 test.py 文件。
運行結果:
可以看到我們用ctypes定義好映射規則,即:Point 類的對象 : a, b, c, d, e
a, b, c, d, e 對象關聯好了Python命名空間下的Python數據類型,當調用c語言庫函數時將按照a,b,c,d,e對象所定義的映射在C語言內存空間下生成對應的C語言數據類型。
兩次調用C庫中的clib.print_point()函數均可以打印C內存空間下的棧中的數據,這充分說明本文前述內容的不充分的地方。C語言內存空間下的數據只要我們加載的動態鏈接庫的接口變量,這里是 clib ,還存在,就是可以一直調用的。
如果我們申請完C語言內存空間后如果刪除clib會不會自動釋放內存呢???
代碼:
import ctypes from ctypes import c_int, c_char, Structure, POINTER, pointer, cdll, byref class Point(Structure): pass Point._fields_ = [('x', c_int), ('y', c_char), ('next', POINTER(Point))] clib = cdll.LoadLibrary('./test.so') clib.new_point.argtype = POINTER(Point) clib.new_point.restype = None clib.print_point.argtype = None clib.print_point.restype = None l = [] for i in range(10000*10000): l.append( Point(i, b'a', None) ) clib.new_point(byref(l[-1]))
其中,test.so 鏈接庫為本文前面所給。
在IDE執行:
內存占用:
刪除 clib 后查看內存情況:
可以發現前面說的又有不對的地方,如果刪除clib變量了,但是C語言內存空間還是沒有釋放,看來最終的答案是C語言內存空間如果申請了就需要設置相應的C函數進行釋放,如果沒有進行C函數釋放那么在Python程序的生命周期內C語言內存空間所申請的空間都是會一直存在的。
於是再次改test.c的代碼增加free_point函數:
void new_point(struct Point *p) { if (p) { if(current==NULL) //是第一個element { head=p; current=p; printf("first add !!! \n"); } else //不是第一個element { current->p=p; current=p; printf("not first add !!! \n"); } // } }
完整的test.c 代碼:

//test.c #include <stdio.h> #include <stdlib.h> // Point 結構體 struct Point { int x; char y; struct Point *p; }; static struct Point* head = NULL; static struct Point* current = NULL; static struct Point* walk = NULL; void new_point(struct Point *p) { if (p) { if(current==NULL) //是第一個element { head=p; current=p; printf("first add !!! \n"); } else //不是第一個element { current->p=p; current=p; printf("not first add !!! \n"); } // } } void print_point() { walk=head; if(walk==NULL) { printf("error, it is a empty list!!! \n"); } else { while(walk!=current) { printf("x: %d, y: %c \n", walk->x, walk->y); walk=walk->p; } if(walk!=NULL) { printf("x: %d, y: %c \n", walk->x, walk->y); } } } void free_point() { while(head!=NULL) { walk=head; head=head->p; printf("begin delete one element !!! \n"); printf("x: %d, y: %c \n", walk->x, walk->y); free(walk); printf("success delete one element !!! \n"); //printf(" %x \n", walk); } } void main() { struct Point a; a.x=97; a.y='a'; a.p=NULL; struct Point b; b.x=98; b.y='b'; b.p=NULL; struct Point c; c.x=99; c.y='c'; c.p=NULL; struct Point d; d.x=100; d.y='d'; d.p=NULL; struct Point e; e.x=101; e.y='e'; e.p=NULL; new_point(&a); new_point(&b); new_point(&c); new_point(&d); new_point(&e); print_point(); }
完整的test.py代碼:

import ctypes from ctypes import c_int, c_char, Structure, POINTER, pointer, cdll, byref class Point(Structure): pass Point._fields_ = [('x', c_int), ('y', c_char), ('next', POINTER(Point))] clib = cdll.LoadLibrary('./test.so') clib.new_point.argtype = POINTER(Point) clib.new_point.restype = None clib.print_point.argtype = None clib.print_point.restype = None clib.free_point.argtype = None clib.free_point.restype = None l = [] for i in range(10000): l.append( Point(i, b'a', None) ) clib.new_point(byref(l[-1])) print("-"*50) #clib.print_point() clib.free_point()
這個釋放內存的函數邏輯十分的清晰,但是運行起來卻報錯。
錯誤的提示也很明白,那就是不能用free來釋放這塊內存,查了好久的C語言語法發現這么寫沒有語法問題,雖然C語言已經是10多年前學的東西了,不過這么簡單的邏輯不應該出錯,這也是十分的不解。
最后在網上看到有人總結了這么一句C語言釋放堆內存的解釋,十分受用,那就是——“誰申請,誰釋放”
在前面的操作中我們刪出了clib變量,那么就是不能再利用C語言動態鏈接庫文件中定義的函數來操作數據了,但是此時並不會釋放C內存空間中的內存,那我們如果把和C內存空間相關聯的ctypes變量刪除,那就是說我們利用ctypes變量映射Python變量的方式使用隱方式生成C語言內存空間下對應的變量(調用動態鏈接庫中的函數自動映射的在C內存空間下生成的數據),那么我們刪除掉這個映射關系,Python中的ctypes會不會本身就存在垃圾回收函數,在Python的垃圾回收機制下自動的回收在C內存空間下生成的堆空間呢???
於是操作:
最終發現,設置ctypes下的數據類型雖然只是定義了一種映射關系,並不能在C語言內存空間下生成對應的變量,最后還是需要調用C內存空間下的函數才能生成對應的C類型數據變量,但是由於C內存空間與Python內存空間是隔離的,我們不能直接操作C內存空間下的數據,而C內存下的數據本身又遵守“誰生成誰銷毀”的原則,這又導致我們無法利用C語言下的free函數來釋放對應的變量空間(這些變量空間是ctypes下定義的數據類型在調用C動態庫中函數自動由Python的ctypes生成的),因此,我們只有利用Python的語言機制和ctypes的語言機制來對C內存空間下的變量進行釋放了,於是我們刪除掉對應的ctypes數據變量也就是刪除了Python變量與C變量的關聯,這樣自然就可以觸發Python語言下的垃圾回收機制來釋放內存。最后的總結還是那句,C語言下的內存申請就是誰申請誰負責釋放。
================================================================