浅析python如何实现类似C++一样调用动态库
作者:越甲八千
多种实现方案,核心思路是将可变的核心业务逻辑抽离到动态库/可动态加载的模块中,主程序EXE仅保留固定的加载和调用逻辑,后续更新只需替换动态库,无需重新打包EXE。
下面详细介绍几种可行方案,从简单到进阶,兼顾易用性和实用性:
一、方案1:最简易——Python动态加载.py模块(无需编译,快速落地)
这是最轻量化的方案,无需处理编译相关的复杂操作,核心是利用Python的importlib标准库,实现运行时动态加载/重新加载.py模块,主程序打包为EXE后,只需替换.py模块文件即可更新逻辑。
核心原理
- 主程序(打包为EXE):仅负责动态加载业务模块、提供调用入口,逻辑固定不变;
- 业务模块(单独存放的
.py文件):存放可变的核心逻辑(如你的手机拍照测试数据处理、上传逻辑); - 后续更新:直接修改/替换
.py业务模块,无需重新打包EXE,主程序下次运行(或热重载)即可加载新逻辑。
实现步骤
步骤1:拆分主程序和业务模块
业务模块:business_logic.py(可变,后续可更新,不打包进EXE)
# 核心业务逻辑(示例:手机测试结果处理)
def handle_test_result(phone_sn, photo_path):
"""处理手机拍照测试结果(可变逻辑,后续可优化更新)"""
print(f"正在处理SN号:{phone_sn} 的测试照片:{photo_path}")
# 此处可放置实际的上传、数据校验等逻辑
result = {
"code": 0,
"msg": "处理成功",
"phone_sn": phone_sn,
"photo_path": photo_path,
"handle_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
return result
def get_test_version():
"""返回业务模块版本(便于确认是否更新成功)"""
return "v1.0.0"
主程序:main.py(固定逻辑,后续打包为EXE)
import importlib
import os
import sys
from datetime import datetime
def load_business_module(module_name="business_logic"):
"""
动态加载业务模块
:param module_name: 业务模块名称(无需.py后缀)
:return: 加载成功的模块对象
"""
# 确保业务模块在可搜索路径中(优先当前EXE运行目录)
exe_dir = os.path.dirname(sys.executable) if hasattr(sys, 'frozen') else os.getcwd()
sys.path.append(exe_dir)
try:
# 动态导入模块(支持重新加载,避免缓存)
if module_name in sys.modules:
return importlib.reload(sys.modules[module_name])
else:
return importlib.import_module(module_name)
except ModuleNotFoundError:
raise Exception(f"未找到业务模块 {module_name}.py,请将其放置在EXE同目录下")
except Exception as e:
raise Exception(f"加载业务模块失败:{str(e)}")
if __name__ == "__main__":
# 固定逻辑:加载业务模块 + 调用核心方法
try:
# 1. 动态加载业务模块
business_mod = load_business_module()
# 2. 调用业务模块的方法(固定调用接口,业务模块内部可更新)
print(f"当前业务模块版本:{business_mod.get_test_version()}")
test_result = business_mod.handle_test_result("SN123456789", "./test_photo.jpg")
print(f"测试结果处理完成:{test_result}")
except Exception as e:
print(f"程序运行失败:{str(e)}")
input("按回车键退出...")
步骤2:打包主程序为EXE
使用pyinstaller打包main.py,注意排除业务模块business_logic.py:
# 安装pyinstaller pip install pyinstaller # 打包命令(--onefile打包为单个EXE,--exclude-module排除业务模块) pyinstaller -F main.py --exclude-module business_logic
步骤3:运行与更新
- 打包完成后,将
business_logic.py复制到EXE所在目录; - 运行EXE,即可正常调用业务模块逻辑;
- 后续更新:直接修改
business_logic.py(如优化handle_test_result逻辑、更新版本号),替换EXE同目录下的该文件,重新运行EXE即可加载新逻辑,无需重新打包。
方案优势与局限
- 优势:零编译、操作简单、学习成本低,适合快速迭代的业务逻辑;
- 局限:
.py文件源码暴露,安全性较低;仅支持Python语法,无法调用C/C++等底层逻辑。
二、方案2:进阶——调用Python编译的.pyd动态库(隐藏源码,性能提升)
.pyd文件是Python的动态链接库(Windows平台,对应Linux的.so文件),本质是将Python代码编译为二进制动态库,既可以隐藏源码,又能实现「更新无需重打包EXE」的效果,核心逻辑与方案1一致,只是将.py模块替换为.pyd动态库。
核心原理
- 将可变业务逻辑的
.py文件编译为.pyd动态库; - 主程序(EXE)通过
importlib动态加载.pyd文件,调用方式与.py模块完全一致; - 后续更新:只需编译新的
.pyd文件,替换EXE同目录下的旧文件即可,无需重打包EXE。
实现步骤
步骤1:编译.py业务模块为.pyd
使用cython将business_logic.py编译为.pyd(隐藏源码+提升运行性能):
安装依赖:
pip install cython
创建编译脚本setup.py:
from setuptools import setup
from Cython.Build import cythonize
import os
# 要编译的Python文件
py_file = "business_logic.py"
setup(
name="PhoneTestBusinessLogic",
ext_modules=cythonize(
py_file,
language_level=3, # 对应Python3版本
annotate=False # 是否生成注释文件,默认False
),
zip_safe=False
)
执行编译命令:
python setup.py build_ext --inplace
编译完成后,会在当前目录生成business_logic.cp3x-win_amd64.pyd(cp3x对应你的Python版本,如cp39对应Python3.9),可将其重命名为business_logic.pyd(方便调用)。
步骤2:主程序加载.pyd动态库
主程序main.py无需修改(与方案1完全一致),importlib会自动识别.pyd文件并加载,调用方式与.py模块无差异。
步骤3:打包与更新
- 打包
main.py为EXE(排除business_logic相关文件); - 将
business_logic.pyd复制到EXE同目录下,运行EXE即可正常调用; - 后续更新:修改
business_logic.py后,重新编译为.pyd,替换旧文件即可,无需重打包EXE。
方案优势与局限
- 优势:源码隐藏、运行性能比纯
.py高、调用方式兼容,兼顾安全性和易用性; - 局限:需要掌握
cython编译技巧,跨平台兼容性稍差(Windows为.pyd,Linux为.so)。
三、方案3:终极——调用C/C++编译的动态库(.dll/.so,高性能+跨语言)
这是与C++调用动态库完全一致的方案,Python通过ctypes(标准库)或cffi(第三方库)调用C/C++编译的.dll(Windows)/.so(Linux)动态库,核心业务逻辑用C/C++实现并编译为动态库,主程序(Python EXE)仅负责调用动态库接口,后续更新只需替换.dll/.so文件,无需重打包EXE。
核心原理
- C/C++编写核心业务逻辑,暴露标准C接口(避免C++名称修饰问题);
- 将C/C++代码编译为
.dll/.so动态库; - Python主程序通过
ctypes加载动态库,调用暴露的接口; - 后续更新:修改C/C++代码,重新编译为动态库,替换旧文件即可,无需重打包Python EXE。
实现步骤(以Windows平台.dll为例)
步骤1:C/C++编写并编译.dll动态库
编写C代码:business_core.c(核心业务逻辑,暴露C接口)
#include <stdio.h>
#include <time.h>
#include <string.h>
// 定义返回结果的结构体(与Python端对应)
typedef struct {
int code;
char msg[128];
char phone_sn[64];
char photo_path[256];
char handle_time[32];
} TestResult;
// 暴露C接口(extern "C" 避免C++名称修饰,C代码可省略)
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif
// 获取当前时间字符串
static void get_current_time(char *time_buf, int buf_size) {
time_t now = time(NULL);
struct tm *t = localtime(&now);
strftime(time_buf, buf_size, "%Y-%m-%d %H:%M:%S", t);
}
// 核心业务接口:处理手机测试结果
DLL_EXPORT void handle_test_result(const char *phone_sn, const char *photo_path, TestResult *result) {
// 初始化结果结构体
memset(result, 0, sizeof(TestResult));
// 填充结果数据
result->code = 0;
strcpy(result->msg, "处理成功");
strcpy(result->phone_sn, phone_sn);
strcpy(result->photo_path, photo_path);
get_current_time(result->handle_time, sizeof(result->handle_time));
printf("正在处理SN号:%s 的测试照片:%s\n", phone_sn, photo_path);
}
// 核心业务接口:获取版本号
DLL_EXPORT const char *get_test_version() {
return "v1.0.0 (C/C++ DLL)";
}
编译为.dll(使用MinGW或MSVC,以MinGW为例):
# MinGW编译命令:生成business_core.dll gcc -shared -o business_core.dll business_core.c -Wall
编译完成后,得到business_core.dll动态库。
步骤2:Python主程序调用.dll动态库
主程序main.py(固定逻辑,打包为EXE):
import ctypes
import os
import sys
# 定义与C结构体对应的Python类(通过ctypes映射)
class TestResult(ctypes.Structure):
_fields_ = [
("code", ctypes.c_int),
("msg", ctypes.c_char * 128),
("phone_sn", ctypes.c_char * 64),
("photo_path", ctypes.c_char * 256),
("handle_time", ctypes.c_char * 32)
]
def load_c_dll(dll_name="business_core.dll"):
"""动态加载C/C++ DLL动态库"""
# 确保DLL在EXE运行目录
exe_dir = os.path.dirname(sys.executable) if hasattr(sys, 'frozen') else os.getcwd()
dll_path = os.path.join(exe_dir, dll_name)
if not os.path.exists(dll_path):
raise Exception(f"未找到动态库 {dll_path},请将其放置在EXE同目录下")
try:
# 加载DLL
dll = ctypes.WinDLL(dll_path)
return dll
except Exception as e:
raise Exception(f"加载动态库失败:{str(e)}")
if __name__ == "__main__":
try:
# 1. 加载C/C++ DLL动态库
dll = load_c_dll()
# 2. 配置DLL接口的参数和返回值类型
# 配置get_test_version接口
dll.get_test_version.restype = ctypes.c_char_p
# 配置handle_test_result接口
dll.handle_test_result.argtypes = [
ctypes.c_char_p, # phone_sn(C字符串)
ctypes.c_char_p, # photo_path(C字符串)
ctypes.POINTER(TestResult) # 结果结构体指针
]
dll.handle_test_result.restype = None
# 3. 调用DLL接口
# 调用获取版本号
version = dll.get_test_version().decode("utf-8")
print(f"当前动态库版本:{version}")
# 调用处理测试结果接口
test_result = TestResult()
phone_sn = b"SN123456789"
photo_path = b"./test_photo.jpg"
dll.handle_test_result(phone_sn, photo_path, ctypes.byref(test_result))
# 解析结果并打印
print("\n测试结果处理完成:")
print(f" 状态码:{test_result.code}")
print(f" 提示信息:{test_result.msg.decode('utf-8')}")
print(f" 手机SN号:{test_result.phone_sn.decode('utf-8')}")
print(f" 照片路径:{test_result.photo_path.decode('utf-8')}")
print(f" 处理时间:{test_result.handle_time.decode('utf-8')}")
except Exception as e:
print(f"程序运行失败:{str(e)}")
input("按回车键退出...")
步骤3:打包与更新
用pyinstaller打包main.py为EXE:
pyinstaller -F main.py
将business_core.dll复制到EXE同目录下,运行EXE即可正常调用C/C++动态库逻辑;
后续更新:修改business_core.c代码,重新编译为business_core.dll,替换旧文件即可,无需重新打包Python EXE。
方案优势与局限
- 优势:高性能(C/C++编译执行)、源码完全隐藏、支持跨语言调用(与C++项目无缝衔接)、更新灵活;
- 局限:需要掌握C/C++开发和动态库编译技巧,调试难度较高,跨平台需分别编译
.dll和.so。
四、核心总结与方案选型建议
三种方案核心对比
| 方案 | 动态库类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 方案1 | .py模块 | 零编译、易上手、快速迭代 | 源码暴露、性能一般 | 内部测试、快速迭代、无源码保密需求 |
| 方案2 | .pyd动态库 | 源码隐藏、性能提升、调用兼容 | 需Cython编译、跨平台稍差 | 有源码保密需求、核心逻辑为Python编写 |
| 方案3 | .dll/.so动态库 | 高性能、源码安全、跨语言 | 需C/C++开发、编译复杂 | 高性能需求、与C++项目对接、严格源码保密 |
核心要点(实现「无需更新EXE」的关键)
- 主程序(EXE)仅保留固定的加载/调用逻辑,不包含可变的核心业务逻辑;
- 可变逻辑抽离到独立的动态模块/动态库中,与EXE分离存放;
- 主程序通过「动态加载」方式调用模块/动态库,不进行静态打包;
- 后续更新仅替换独立的动态模块/动态库,无需改动主程序EXE。
选型建议
- 若你是Python开发者,无C/C++基础,优先选择方案1(快速落地)或方案2(需要保密时);
- 若你有C/C++基础,或需要高性能、跨语言对接,优先选择方案3(与C++调用动态库逻辑一致);
- 无论哪种方案,核心原则是「分离固定逻辑和可变逻辑」,这是实现「无需更新EXE」的根本。
到此这篇关于浅析python如何实现类似C++一样调用动态库的文章就介绍到这了,更多相关python调用动态库内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
