python 中 ctypes 的使用嘗試


最近在看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();


}
View Code

 

完整的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()
View Code

 

這個釋放內存的函數邏輯十分的清晰,但是運行起來卻報錯。

 

 

錯誤的提示也很明白,那就是不能用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語言下的內存申請就是誰申請誰負責釋放。

 

 

 

================================================================ 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM