はじめに
Python で自作パッケージを作る際に必要となる手順や注意点を、備忘録としてまとめた。
特に今回は ctypes を使って C のライブラリを読み込むパッケージを作るという、少し特殊なケースを扱ってみる。
目次
モジュールの呼び出し方
pythonファイルを呼び出す場合は、importとfromを使う。
-
同じ階層にあるpythonファイルを呼び出す場合
import [ファイル名]で呼び出す。
module_test.pyからmodule.pyのlib_python()を呼び出す場合は下記のようになる。
def lib_python():
print('call:lib_python()')
import module
module.lib_python()
-
サブフォルダのpythonファイルを呼び出す場合
fromでフォルダパスを指定する。
但し、フォルダ配下に__init__.pyファイルが必要になる。このファイルでフォルダー配下にpythonファイルがあることを認識する仕組みになっている。__init__.pyはimportされてた時に呼ばれる。ファイルは空ファイルでも良いし、初期化処理を記載しても良い。
project
├── lib/
│ ├── __init__.py
│ └── module.py
└── module_test.py
def lib_python():
print('call:lib_python()')
from lib import modoule
module.lib_python()
if _name_ == '_main_' の役割
Pythonでよく使われるこの記述は、単なるおまじないではなく、モジュール化を意識した設計に基づいている。
def function():
print('call:function')
if __name__ == '__main__':
function()
ここで登場する __name__ は、モジュールの名前を表す特別な変数です。たとえば、モジュールを他のファイルからインポートした場合、 __name__ にはモジュール名が代入され、スクリプトで直接実行した場合には __main__ が代入される。
以下のコードを実行すると、sys.__name__ は sys、__name__ は __main__ と出力される。
import sys
def main():
print('call:main()')
if __name__ == '__main__':
print(f'{sys.__name__=}')
print(f'{__name__=}')
# sys.__name__='sys'
# __name__='__main__'
この仕組みだけを見ると「何に使うの?」となりますが、自作モジュールを他のファイルから再利用する際にとても重要な役割を果たす。
-
if name == 'main':を使わない場合
たとえば、module.pyというモジュールを作成し、それをmodule_test.pyからインポートすると、module.py 内のprint(f'hello!')がインポート時にも実行されてしまう。
def lib_python():
print('call:lib_python')
print(f'hello!') #ここが実行されてしまう
import module
def main():
module.lib_python()
if __name__ == '__main__':
main()
hello!
call:lib_python
-
if name == 'main':を使った場合
今度は module.py のprint(f'hello!')をif __name__ == '__main__':の中に入れる。すると、直接実行したときだけ呼ばれ、インポート時には実行されない。
def lib_python():
print('call:lib_python')
if __name__ == '__main__':
print(f'hello!') #自身を実行するときのみコール。
import module
def main():
module.lib_python()
if __name__ == '__main__':
main()
call:lib_python
このように、if __name__ == '__main__': を使うことで、モジュールとしての再利用性を高めつつ、直接実行時のテストコードなども安全に書けるようになる。
C言語でライブラリ化:cytpes
Python の処理を高速化したい場合や、Python では直接扱いにくいデバイス制御・ドライバ連携を行いたい場合、対象となる処理を C 言語で実装し、その関数を Python から呼び出すという方法がある。
このときに便利なのがctypesで、C言語で作成した動的ライブラリ(.so)を Python から読み込み、関数をそのまま呼び出すことができる。
[参考にさせてもらった記事]
-
ライブラリの呼び出し方
Cライブラリを呼び出すPythonコード。lib_c.cファイルをgcc lib_c.c -shared -o lib_c.soでコンパイルして動的ライブラリ化している前提。
import ctypes
# 呼び出すライブラリのファイル名を指定
lib = ctypes.cdll.LoadLibrary("./lib_c.so")
# int func(int a); という関数呼び出す場合
ret = lib.func(100) #引数、戻り値の型設定は適宜実施。
#func()は引数、戻り値ともにint型なので型指定せずに直で設定している。
# 結果
print(ret) # 10000
int func(int a){
return a*100;
}
//例なので単純に100倍した値を返答しているが、Pythonで実装難しい処理をさせる。
- ライブラリの引数と戻り値の型(例)
| C型 | Python型 |
|---|---|
| char | c_char/c_byte |
| short | c_short |
| int | c_int |
| long | c_long |
| unsigned char | c_ubyte |
| unsigned short | c_ushort |
| unsigned int | c_uint |
| unsigned long | c_ulong |
| float | c_float |
| double | c_double |
| char* (NUL terminated) | c_char_p |
| void * | c_void_p |
引数の型設定
# int
i = ctypes.c_int(100)
# float
f = ctypes.c_float(100.15)
# unsigned char 型の配列
list = (ctypes.c_ubyte * 10)(0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a)
# 16進数データ列
bytearray_list=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a'
# 文字列
str = b'hello!'
戻り値の型設定
# 戻り値------------
# int
ret = ctypes.c_int()
# uinsgned char型のポインタ
ret = ctypes.POINTER(ctypes.c_ubyte)
パッケージ化
パッケージ化の手順は 1〜4 が「作る側」、5 が「使う側」 の作業になる。順番に説明する。
- パッケージ化したいPythonコードを準備する。
- 公開したい API を
__init__.pyに記述する。 setup.pyにパッケージ化に必要な情報を記述する。- distにする。
- 作成したパッケージを pip でインストールする。
project
├── dist/
│ └── sample_package-0.0.1.tar.gz -> 4.dist生成結果
├── sample_package/
│ ├── clib/ -> 1.Cライブラリ
│ │ ├── lib_c.c
│ │ └── lib_c.so
│ ├── __init__.py -> 2.公開API設定
│ └── lib.py -> 1.パッケージ化するコード
└── setup.py -> 3.パッケージ情報記述
1.パッケージ化したいPythonコードを準備する。
まず、Python 側の処理と C 言語側の処理を用意する。
例では、引数の値を 100 倍にして返す処理を Python 関数 func_python() と C 言語用関数 func_c() の両方で実装している。func_c() は、あらかじめコンパイルしておいた lib_c.so を ctypes で読み込んで実行する。
- 重要な注意点
C のライブラリ(.so)のパス指定にはimportlib.resourcesを使うこと。相対パス(例:"./clib/lib_c.so")を使うと、パッケージを pip install した後の環境では正しくロードできない。
import ctypes
import sys
import importlib.resources
# Pythonで val を 100倍にする
def func_python(val):
return f'Python_function : {val*100 = }'
# Cで val を 100倍にする
def func_c(val):
# 呼び出すライブラリのファイル名を指定(相対パスでは×)
with importlib.resources.path("sample_package.clib", 'lib_c.so') as path:
lib = ctypes.CDLL(str(path))
return f'C lang_function : {lib.func(val) = }'
# 公開API
def pkg_api(lang_type, val):
if(lang_type=='python'):
ret=func_python(val)
elif(lang_type=='c'):
ret=func_c(val)
else:
ret = f'{lang_type} in not available.'
return ret
if __name__ == '__main__':
# test
print(pkg_api('python',10))
print(pkg_api('python',50))
print(pkg_api('c',10))
print(pkg_api('c',50))
print(pkg_api('java',10))
print(pkg_api('java',50))
int func(int val){
return val*100;
}
gcc lib_c.c -shared -o lib_c.so でコンパイルしてlib_c.soを生成する。
戻る
2.公開したい API を__init__.pyに記述する。
パッケージ化を行う場合、フォルダ名とパッケージ名を一致させる必要がある。今回の例ではパッケージ名を 「sample_package」 としている。
次に、パッケージの外部に公開したい API を __init__.py に列挙する。ここでは pkg_api() を公開したいので、__init__.py に以下のように記述する。
- from には、公開 API が定義されているモジュール(lib.py)のパス
- import には、公開したいAPI関数(pkg_api())を指定する。
from .lib import pkg_api
3.setup.pyにパッケージ化に必要な情報を記述する。
パッケージを配布するためには、setup.py にパッケージ名・バージョン・含めるファイルなどの情報を記述する。今回の例では C 言語で作成したライブラリ(.so)をパッケージに含めるため、次の 2 つの設定が必須になる。
- package_data : Cライブラリのフォルダ指定。
- include_package_data=True
これらを指定しないと、clib/ ディレクトリ内の共有ライブラリが読み込まれず、インストール後に ctypes がロードできなくなる。以下は今回の例に対応したsetup.py。
from setuptools import setup, find_packages
setup(
name = 'sample_package',
version = '0.0.1',
packages = find_packages(),
package_data = {'sample_package':['clib/*'],},
include_package_data = True,
url = 'https://xxxx.sample.com',
license = 'free',
author = 'name',
author_email = 'xxxx@sample.com',
description = 'A simple example package for learning Python packaging.'
)
4.distにする。
パッケージ化の準備が整ったら、sdist(ソース配布物)を作成する。以下のコマンドを実行すると、プロジェクト直下の dist/ フォルダにsample_package-0.0.1.tar.gzが生成される。
$ python setup.py sdist
5.作成したパッケージを pip でインストールする。
作成したパッケージは、pip install を使ってローカル環境にインストールできる。以下のコマンドを実行すると、dist/ にある sample_package-0.0.1.tar.gz がインストールされる。
$ pip install ./dist/sample_package-0.0.1.tar.gz
インストールが正常に行われたかどうかは、pip show で確認できる。
$ pip show sample_package
👇パッケージがインストールされていれば次のように表示される
Name: sample_package
Version: 0.0.1
Summary: A simple example package for learning Python packaging.
Home-page: https://xxxx.sample.com
Author: name
Author-email: xxxx@sample.com
License: free
Location: \xxxxx\Python\Python313\Lib\site-packages
Requires:
Required-by:
sample_package を import し、公開 API の pkg_api() を呼び出すと次のように動作する。
import sample_package
print(sample_package.__name__)
print(sample_package.pkg_api('python',10))
print(sample_package.pkg_api('c',10))
print(sample_package.pkg_api('java',10))
#-----実行結果-----
# sample_package
# Python_function : val*100 = 1000
# C lang_function : lib.func(val) = 1000
# java in not available.