1. ホーム
  2. python

pythonはgoまたはcを呼び出す

2022-02-19 06:48:08
<パス <ブロッククオート

この文書は、リトル・ミンが個人的な学習のために編集したものです。

記事のリンク https://blog.csdn.net/as604049322/article/details/112058313

pdfダウンロードはこちら https://download.csdn.net/download/as604049322/13999212

go言語へのpythonの呼び出し

Pythonは、最高の効率でほとんどのことを行うことができる、非常に生産的な言語ですが、Pythonのパフォーマンスは、我々は、特に大きなロックGILを批判されている問題です。もちろん、最近ほとんどのプログラムは、(IO)ネットワーク集約型のプログラムであり、Pythonはそのために十分ですが、もし我々が既に存在するプロジェクトや開発したい、そこに計算集約プログラムのシナリオを言う、パフォーマンスを改善するためには何ができるのですか?

一般的には、Pythonの計算負荷の高い部分をC++で書き換えて性能を向上させることができますが、C++は学習コストやポインタ、自分でメモリを解放する敷居の高さがあり、自動ガベージコレクションやもともと並行性が高いというメリットがあるGoが重宝されるんですね。

pythonのctypesモジュールはC互換のデータ型とso/dllダイナミックリンクライブラリファイルを読み込む関数を提供し、GO言語自体はこの2つの機能を元にC仕様に準拠したdllやsoダイナミックリンクライブラリをコンパイルできるので、パイソンを使って問題なくGO言語が呼び出せるのです。

Golangの環境設定

Goの公式ミラーサイトです。 https://golang.google.cn/dl/

デフォルトの最高バージョンを選択するだけで、Go コードは後方互換性があります。 は、その バージョン間の差は関係ない

インストールが正常に行われたかどうかを確認する

>go version
go version go1.15.2 windows/amd64


すでにバージョン1.11以上なので、今回は go mod を設定することなく、依存関係を管理することができます。 GOPATH などと変なことを言っています。

GOPROXY(プロキシ)の設定

Goからパッケージを借りてダウンロードしたりする必要があるかもしれませんが、デフォルトの公式ソースの GOPROXY=https://proxy.golang.org,direct 中国国内ではアクセスできない

入る go env Goのコンフィギュレーションを表示する。

>go env
...
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=D:\Go
...


国内ミラーサイトへ変更

go env -w GOPROXY=https://goproxy.cn,direct


もう一度、Goの設定を見る。

>go env
...
set GOPROXY=https://goproxy.cn,direct
set GOROOT=D:\Go
...


go言語クロスプラットフォームコンパイル

クロスプラットフォームコンパイルとも呼ばれ、WinプラットフォームでLinuxの実行ファイルにコンパイルすることができます。

これがGoの人気の理由です。java、python、phpなどの言語は一般的にwinプラットフォームで開発され、linuxに展開されるため、サードパーティの依存関係に対応するのが大変なんです。

Goを使えば、サードパーティの依存関係がどうであれ、最終的には実行ファイルにパッケージされ、直接、瞬時に、しかも高度に並列化された形でデプロイされるのです。

Linuxプラットフォームの実行ファイルをWindowsでコンパイルする。

cmd の下で、以下のコマンドを順に実行します。

SET CGO_ENABLED=0 // Disable CGO
SET GOOS=linux // the target platform is linux
SET GOARCH=amd64 // target processor architecture is amd64


次に、以下を実行します。 go build をクリックすると、Linux上で実行可能なファイルが得られます。

このファイルをlinuxのサーバーにアップロードすると、Go環境がなくても正常に実行できる。

Macプラットフォーム用の64ビット実行ファイルをWindowsでコンパイルする。

SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build


Mac上のLinuxおよびWindowsプラットフォーム用の64ビット実行ファイルをコンパイルします。

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build


MacおよびWindowsプラットフォーム用の64ビット実行ファイルをLinuxでコンパイルします。

CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build


PythonとGoのパフォーマンス比較

最適化の効果をよりよく示すために、計算量の多いケースで2つの言語の違いを大まかに比較してみましょう。

テスト 大規模計算の累積シミュレーションを10億回(100000000回)個別に計算する。

Pythonのコードです。

import time

def run(n):
    sum = 0
    for i in range(n):
        sum += i
    print(sum)


if __name__ == '__main__':
    startTime = time.time()
    run(100000000)
    endTime = time.time()
    print("elapsed time:", endTime - startTime)


に5sほどかかった。

囲碁のコード

package main

import (
  "fmt"
  "time"
)

func run(n int) {
  sum := 0
  for i := 0; i < n; i++ {
    sum += i
  }
  fmt.Println(sum)
}
func main() {
  var startTime = time.Now()
  run(100000000)
  fmt.Println("elapsed time:", time.Since(startTime))
}


50ms程度かかります。

Python呼び出し可能な.soファイルへコンパイルされたGoコード

64ビット版gccツールMinGWのインストール

https://sourceforge.net/projects/mingw-w64/下载后 にアクセスし、ステップバイステップでインストールします。

オフラインのパッケージがBaidu Cloudにアップロードされました。

https://pan.baidu.com/s/1ZmjQUf5QcBbeHCi7mIrYxg 抽出コード:edc5

Windowsはx86_64-8.1.0-release-win32-seh-rt_v6-rev0に対応、直接解凍して環境変数にMinGWのbinディレクトリを追加すれば使えるようになります。

gccのバージョンを表示するには。

>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=D:/develop/mingw64/bin/... /libexec/gcc/x86_64-w64-mingw32/8.1.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ... /... /... /src/gcc-8.1.0/configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --with- sysroot=/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64 --enable-shared --enable-static --disable-multilib --enable-languages=c, c++,fortran,lto --enable-libstdcxx-time=yes --enable-threads=win32 --enable-libgomp --enable-libatomic --enable-lto --enable-graphite -- enable-checking=release --enable-fully-dynamic-string --enable-version-specific-runtime-libs --disable-libstdcxx-pch --disable- libstdcxx-debug --enable-bootstrap --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-gnu- as --with-gnu-ld --with-arch=nocona --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/c/mingw810/prerequisites/x86_64-w64 --mingw32-static --with-mpfr=/c/mingw810/prerequisites/x86_64-w64 --mingw32-static --with-mpc=/c/mingw810/prerequisites/x86_64-w64- mingw32-static --with-isl=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-pkgversion='x86_64-win32-seh-rev0, Built by MinGW- W64 project' --with-bugurl=https://sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt _v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32- static/include' CXXFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-win32-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/ prerequisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_

.soファイルでコンパイルする必要があるgoのコードには、例えば、Cをインポートする必要があるなど、いくつかの要件があります。

package main

import (
  "C" //C must be imported
)

//export run
func run(n int) int{
  // The function must be externally interfaced via a comment in the export function name format
  sum := 0
  for i := 0; i < n; i++ {
    sum += i
  }
  fmt.Println("I'm Go code, I'm done running, my result is:",sum)
  return sum
}

func main() {
  //main function do not write anything, and package name main to correspond to
}


Python 呼び出し用に .so ファイルにコンパイルします。

go build -buildmode=c-shared -o s1.so s1.go


形式を指定します。 go build -buildmode=c-shared -o output .so file go source file

以下のように.hファイルと.soファイルが生成され、.soファイルはPythonの呼び出しに利用できるようになります。

Ptyhonはsoファイルを呼び出します

上記で生成された.soファイルをPythonプロジェクトの同じ階層にコピーします。

書き方 s1.py で、やはり10億を計算し、肝心の部分はGoで生成された.soで実行されます。

from ctypes import *
import time

if __name__ == '__main__':
    startTime = time.time()

    s = CDLL("s1.so") # load s1.so file
    result = s.run(100000000) # call the run function inside the .so file generated by Go
    print("result:", result)

    endTime = time.time()
    print("elapsed time:", endTime - startTime)


総経過時間:約0.04s。

見ての通り、Python は高速ですが、Go が生成した .so ファイルを呼び出した後、驚くことに間違った戻り値を表示します。

しかし、例えば10023のように、いくつかの小さな数字を計算すると正しい結果が得られます。

.hファイルの探索

上記の問題は、デフォルトの戻り値型ストレージの範囲が限定されていることが原因ですが、以下、goコンパイルで生成されたc中間ファイルを解析することでその原因を探ります。

.hファイルを開いて、最後まで行ってみてください。

extern で始まる宣言を検索します。

extern GoInt run(GoInt n);


これは、先のgoソースコードで宣言されたrunメソッドをcコードに変換したところで、引数型と戻り値型がともにc言語のGoInt型であることを示しています。

型定義に目を向ける。

このように、GoIntは実際にはGoInt64であり、GoInt64はlong long型であることがわかります。

Pythonは ctypes モジュールに対応するテーブルを持つ .so ファイルを呼び出します。

参考:https://docs.python.org/zh-tw/3.7/library/ctypes.html

<テーブル ctypes タイプ C言語タイプ Pythonタイプ c_bool ブール ブール (1) c_char チャー 1文字バイトオブジェクト c_wchar wchar_t 一文字の文字列 c_byte チャー int c_ubyte 符号なし文字 int c_short 短い int c_ushort 符号なしショート int c_int int int c_uint 符号なしint int c_long 長い int c_ulong 符号なしロング int c_longlong __int64またはlong long int c_ulonglong 符号なし__int64または符号なしlong long イント c_size_t size_t int c_ssize_t ssize_t または Py_ssize_t int c_float フロート フロート c_double ダブル フロート c_longdouble ロングダブル フロート c_char_p char * (NULで終わる) バイト列オブジェクトまたは None c_wchar_p wchar_t * (NULで終了) 文字列またはなし c_void_p ボイド int または None

上の表によると、C言語のlong long型に対応するctype型はc_longlongで、pythonの型はintであることがわかりますね。

pythonのデフォルトの値の取り扱い型はLong(8バイト)であり、宣言されていないgo言語でコンパイルされたrunメソッドはInt型(4バイト)の値を返すので、計算結果がIntの格納可能範囲を超えた場合に問題が発生します。

Int の範囲は -2^31 - 2^31-1 で、-2147483648 - 2147483647 となります。

あとは、実際のctypeに基づいて、pythonでrunの実際の戻り値の型を宣言するだけです。

from ctypes import *
import time

if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s1.so") # load s1.so file
    # According to the table, the corresponding ctypes for long long in C are c_longlonglong
    s.run.restype = c_longlong # Declare the return value type of the run function of .so, fixed format
    result = s.run(100000000) # Call the run function in the .so file generated by Go

    print(result)
    endTime = time.time()
    print("elapsed time:", endTime - beginTime)


<イグ

結果は、もう文句なしです。

戻り値が文字列の場合の処理

s2.goのコードです。

package main

import (
	"C" //C must be imported
)

//export speak
func speak(n int) string {
	return "996 so tired ah, a rare day off, take a good rest "
}
func main() {
	//main function do not write anything, and package name main to correspond to
}



s2.hを表示します。

typedef struct { const char *p; ptrdiff_t n; } _GoString_;
typedef _GoString_ GoString;
...
extern GoString speak(GoInt n);
...


上記から、GoStringが _GoString_ 型であるのに対し _GoString_ は、char * と ptrdiff_t の構造体です。

c言語仕様では、ptrdiff_tはマシン依存の データ型 ptrdiff_t 型は 変数 は通常、2つの ポインタ ptrdiff_t はファイル stddef.h (cstddef) で定義されています。ptrdiff_t は通常 long int 型として定義されますが、long long 型として定義することもできます。

表を見ると、構造体c_char_pとc_longlongはpythonで宣言する必要があることがわかります。

class GoString(Structure):
    # typedef struct { const char *p; ptrdiff_t n; } _GoString_;
    # ptrdiff_t == long long
    _fields_ = [("p", c_char_p), ("n", c_longlong)]


s3.pyの完全なコードです。

from ctypes import *
import time


class GoString(Structure):
    # typedef struct { const char *p; ptrdiff_t n; } _GoString_;
    _fields_ = [("p", c_char_p), ("n", c_longlong)]


if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s2.so") # load s1.so file

    s.speak.restype = GoString
    speakStr = s.speak(5)
    # return is byte type, need to convert to string, the return content in .p, .n is the length of the cut
    speakStr = speakStr.p[:speakStr.n].decode("utf-8")
    print("speak:", speakStr)

    endTime = time.time()
    print("elapsed time:", endTime - beginTime)


<イグ

しかし、この上のコードは返された文字列を定数としてしかサポートしておらず、一度goのコードを以下のように変更して、上記の手順を繰り返しています。

s2.goのコードです。

package main

import (
	"C" //C must be imported
	"strconv"
)

//export speak
func speak(n int) string {
	s := "996 so tired ah, a rare day off, rest " + strconv.Itoa(n)
	return s
}

func main() {
	//main function do not write anything, and package name main to correspond
}


上記の手順を繰り返して s3.py と表示され、以下のエラーが発生します。

これは、この時点の文字列がcオブジェクトではなく、goオブジェクトであるためで、以下のコードに修正することができます。

s2.goのコードです。

package main

import (
	"C" //C must be imported
	"strconv"
)

//export speak
func speak(n int) *C.char {
	s := "996 so tired ah, a rare day off, take a good rest " + strconv.Itoa(n)
	return C.CString(s)
}

func main() {
	//main function in what do not write, and package name main to correspond to
}


上記のコードは、cで文字列型を返すと宣言していますが、.hファイルを見ると、以下のようになります。

extern char* speak(GoInt n);


次に s3.pyのコードは、単に次のように変更する必要があります。

from ctypes import *
import time

if __name__ == '__main__':
    beginTime = time.time()
    s = CDLL("s3.so") # load s1.so file

    s.speak.restype = c_char_p
    speakStr = s.speak(7).decode("utf-8")
    print("speak:", speakStr)

    endTime = time.time()
    print("elapsed time:", endTime - beginTime)


<イグ

スムーズに動作する

ctypesを使ったC言語のコードへのアクセス

基本的な例

2つの数値の和を実装したC言語コード add.c ドキュメントをご覧ください。

#include <stdio.h>

int add_int(int, int);
float add_float(float, float);

int add_int(int num1, int num2){
    return num1 + num2;
}

float add_float(float num1, float num2){
    return num1 + num2;
}


として、Cファイルをコンパイルします。 .so ファイルを作成します。

#For Linux or windows
gcc -shared -Wl,-soname,adder -o adder.so -fPIC add.c

#For Mac
gcc -shared -Wl,-install_name,adder.so -o adder.so -fPIC add.c


Pythonのコードで呼び出す場合。

from ctypes import *

adder = CDLL('adder.so')

res_int = adder.add_int(4, 5)
print("Sum of 4 and 5 = " + str(res_int))

add_float = adder.add_float
add_float.restype = c_float
a = c_float(5.5)
b = c_float(4.1)
print("Sum of 5.5 and 4.1 = ", str(add_float(a, b)))


出力します。

Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 = 9.600000381469727


ctypes インターフェースは、ネイティブの Python でデフォルトの文字列や整数の型を使用する C 関数を引数として呼び出すことを可能にしますが、boolean や浮動小数点などの他の型については、正しい ctype 型を使用しなければ実行できません。例えば、呼び出す先が adder.add_float() 関数に渡す場合、Pythonのfloat型をc_floatに変換してからC関数に渡さなければなりません。

複雑な例

というコーディングのcコード sample.cファイル を読み取ることができます。

#include 
The following code is executed on the command line, compiling c.
gcc -shared -o sample.so sample.c

Write the python code in the same directory as the sample.so file.
sample.py file
import ctypes

_mod = ctypes.cdll.LoadLibrary('sample.so')

# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
gcd.restype = ctypes.c_int

# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int

# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int


def divide(x, y):
    rem = ctypes.c_int()
    quot = _divide(x, y, rem)

    return quot, rem.value


# void avg(double *a, int n)
# Define the type of the 'double *' parameter
class DoubleArrayType:
    def from_param(self, param):
        typename = type(param). __name__
        if hasattr(self, 'from_' + typename):
            return getattr(self, 'from_' + typename)(param)
        elif isinstance(param, ctypes.Array):
            return param
        else:
            raise TypeError("Can't convert %s" % typename)

    # Cast from array.array objects
    def from_array(self, param):
        if param.typecode ! = 'd':
            raise TypeError('must be an array of doubles')
        ptr, _ = param.buffer_info()
        return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))

    # Cast from lists/tuples
    def from_list(self, param):
        val = ((ctypes.c_double) * len(param))(*param)
        return val

    from_tuple = from_list

    # Cast from a numpy array
    def from_ndarray(self, param):
        return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))


_avg = _mod.avg
_avg.argtypes = (DoubleArrayType(), ctypes.c_int)
_avg.restype = ctypes.c_double


def avg(values):
    return _avg(values, len(values))


# struct Point { }
class Point(ctypes.Structure):
    _fields_ = [('x', ctypes.c_double),
                ('y', ctypes.c_double)]


# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double

Then it's time to load and use the C functions defined inside, writing
test.py
:
import sample

print("sample.gcd(35, 42):", sample.gcd(35, 42))
print("sample.in_mandel(0, 0, 500):", sample.in_mandel(0, 0, 500))
print("sample.in_mandel(2.0, 1.0, 500):", sample.in_mandel(2.0, 1.0, 500))
print("sample.divide(42, 8):", sample.divide(42, 8))
print("sample.avg([1, 2, 3]):", sample.avg([1, 2, 3]))

p1 = sample.Point(1, 2)
p2 = sample.Point(4, 5)
print("sample.distance(p1, p2):", sample.avg([1, 2, 3]))

Result of execution.
sample.gcd(35, 42): 7
sample.in_mandel(0, 0, 500): 1
sample.in_mandel(2.0, 1.0, 500): 0
sample.divide(42, 8): (5, 2)
sample.avg([1, 2, 3]): 2.0
sample.distance(p1, p2): 2.0

Explanation of complex examples
Loading the c function library
If the C library is installed as a standard library, then the ctypes.util.find_library() function can be used to find where it is located: the
>>> from ctypes.util import find_library
>>> find_library('m')
'libm.so.6'
>>> find_library('pthread')
'libpthread.so.0'
>>> find_library('sample')

If it is a non-standard library, you need to know where the C library is located and then use ctypes.cdll.LoadLibrary() to load it.
_mod = ctypes.cdll.LoadLibrary(_path) #_path is the location of the C library, both full and relative paths are possible

Specify the type of parameters and return values
After the function libraries are loaded, specific symbols need to be extracted to specify their types. For example
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int

In this code, the function's
.argtypes
 attribute is a tuple containing the input arguments to a function, and the 
.restype
 is the return type of the function.
The ctypes defined by c_double, c_int, c_short, c_float, etc. represent the corresponding C data types.
The binding of these type signatures is an important step in order for Python to pass the correct argument types and convert the data correctly. Omitting this type-signature step can cause the code to not run properly, or even hang the entire interpreter process.
The pointer argument needs to be passed in as a ctypes object
Native C code types sometimes don't correspond explicitly to Python, e.g.
# int divide(int, int, int *) in c code
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
# calls in python code
x = 0
divide(10, 3, x)


This way of writing violates Python's immutability principle for integers and can cause the entire interpreter to fall into a black hole.
For arguments involving pointers, it is usually necessary to construct a corresponding ctypes object before passing it in as an argument: the
x = ctypes.c_int()
divide(10, 3, x)

The ctypes.c_int instance is passed in as a pointer, and unlike normal Python integers, the c_int object can be modified.
.value
attribute can be used to get or change this value: the
x.value

For such un-Python-like C calls, it is often possible to write a wrapper function that
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int

def divide(x, y):
    rem = ctypes.c_int()
    quot = _divide(x, y, rem)
    return quot, rem.value

Arguments contain arrays
For the avg() function, the
double avg(double *a, int n)
, the C code expects to receive a pointer to an array of type double and an array length value.
Arrays come in many forms in Python, including lists, tuples, arrays of array modules, numpy arrays, and more.
DoubleArrayType demonstrates how to handle this case.
The method from_param() takes a single argument and then converts it down to a suitable ctypes object.
def from_param(self, param):
    typename = type(param). __name__
    if hasattr(self, 'from_' + typename):
        return getattr(self, 'from_' + typename)(param)
    elif isinstance(param, ctypes.Array):
        return param
    else:
        raise TypeError("Can't convert %s" % typename)

The type name of the argument is extracted and used to distribute it to a more specific method.
For example, if the argument is a list, then typename is list, and the from_list method is called.
def from_list(self, param):
    val = ((ctypes.c_double) * len(param))(*param)
    return val

Demonstrating the conversion of list lists to ctypes arrays via the interactive command line.
>>> import ctypes
>>> nums = [1, 2, 3]
>>> a = (ctypes.c_double * len(nums))(*nums)
>>> a
<__main__.c_double_Array_3 object at 0x10069cd40>
>>> a[0]
1.0
>>> a[1]
2.0
>>> a[2]
3.0

If the argument is a numpy array, then typename is ndarray, and the from_ndarray method is called.
def from_ndarray(self, param):
	return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

If the argument is an array object, then typename is array, and the from_array method is called.
def from_array(self, param):
    if param.typecode ! = 'd':
        raise TypeError('must be an array of doubles')
    ptr, _ = param.buffer_info()
    return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))

For array objects, the buffer_info() method gets the corresponding memory address and length of the array, and ctypes.cast() converts the memory address to a ctypes pointer object.
>>> import array
>>> a = array.array('d',[1,2,3])
>>> a
array('d', [1.0, 2.0, 3.0])
>>> ptr,length = a.buffer_info()
>>> ptr
4298687200
>>> length
3
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
<__main__.LP_c_double object at 0x10069cd40>

By defining the DoubleArrayType class and using it in the avg() type signature, then this function can accept multiple different array-like inputs.
import sample
sample.avg([1,2,3])
2.0
sample.avg((1,2,3))
2.0
import array
sample.avg(array.array('d',[1,2,3]))
2.0
import numpy
sample.avg(numpy.array([1.0,2.0,3.0]))
2.0

The parameters contain the structure
For structures, it is enough to simply define a class containing the appropriate fields and types: the
class Point(ctypes.Structure):
    _fields_ = [('x', ctypes.c_double),
                ('y', ctypes.c_double)]

Type signature bindings require only.
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double

Once a class is defined, it can be used in a type signature or in code that needs to instantiate a structure. For example.
>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> p1.x
1.0
>>> p1.y
2.0
>>> sample.distance(p1,p2)
4.242640687119285

Converting function pointers to callable objects
Get the memory address of a C function (tested and supported on linux, not on windows).
import ctypes
lib = ctypes.cdll.LoadLibrary(None)
# Get the address of the sin() function of the C math library
addr = ctypes.cast(lib.sin, ctypes.c_void_p).value
print(addr)

The above code gets the integer 140266666308000 under linux, while under Windows it reports an error
TypeError: LoadLibrary() argument 1 must be str, not None
With the memory address of the function, it can be converted into a Python callable object.
# Convert the function address into a Python callable object with arguments of the function's return value type and argument type
functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
sin = functype(addr)
print(sin)

The first argument to CFUNCTYPE() is the return type, the next argument is the argument type, and the resulting object is used as a normal function accessible via ctypes.

Print: <CFunctionType object at 0x7f9261becb38>
Calling the test.
>>> import math
>>> math.pi
3.141592653589793
>>> sin(math.pi)
1.2246467991473532e-16
>>> sin(math.pi/2)
1.0
>>> sin(math.pi/6)
0.499999999999999999994
>>> sin(2)
0.9092974268256817
>>> sin(0)
0.0

The techniques involved here are widely used in various advanced code generation techniques, such as on-the-fly compilation, as seen in the LLVM function library.
The following is a brief demonstration of the llvmpy extension, which builds a small aggregate function, gets its function pointer, and then converts it to a Python callable object and executes the function.
>>> from llvm.core import Module, Function, Type, Builder
>>> mod = Module.new('example')
>>> f = Function.new(mod, Type.function(Type.double(), [Type.double(), Type.double()], False), 'foo')
>>> block = f.append_basic_block('entry')
>>> builder = Builder.new(block)
>>> x2 = builder.fmul(f.args[0],f.args[0])
>>> y2 = builder.fmul(f.args[1],f.args[1])
>>> r = builder.fadd(x2,y2)
>>> builder.ret(r)
<llvm.core.Instruction object at 0x10078e990>
>>> from llvm.ee import ExecutionEngine
>>> engine = ExecutionEngine.new(mod)
>>> ptr = engine.get_pointer_to_function(f)
>>> ptr
4325863440
>>> foo = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double, ctypes.c_double)(ptr)
>>> foo(2,3)
13.0
>>> foo(4,5)
41.0
>>> foo(1,2)
5.0

Note: This is dealing directly with machine-level memory addresses and local machine code, not Python functions.
Handling the case where the argument contains a string
Testing the program
str1.c
#include 
Result of execution.
> gcc str1.c&a.exe
Hello
48 65 6c 6c 6f

Compile the c program into a so file.
gcc -shared -o str1.so str1.c

Called with python.
import ctypes

_mod = ctypes.cdll.LoadLibrary('str1.so')
# void print_chars(char *s)
print_chars = _mod.print_chars
print_chars.argtypes = (ctypes.c_char_p,)

print_chars(b'Hello')
print_chars(b'Hello\x00World')

Print the result.
Hello
48 65 6c 6c 6f 
Hello
48 65 6c 6c 6f 

You cannot pass in python string types directly, e.g.
print_chars('Hello World')
Otherwise, an error is reported as
ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type
If you need to pass a string instead of bytes, you can first encode it in UTF-8 to bytes as follows
>>> print_chars('Hello World'.encode('utf-8'))
Hello World
48 65 6c 6c 6f 20 57 6f 72 6c 64 

gcc -shared -o sample.so sample.c