当前位置: 首页 > news >正文

Python程序的「加密」:Cython编译

目录

  • 背景
  • Python程序的「加密」
  • 什么是Cython
  • Cython用法之setup脚本
    • 安装Cython
    • 编译前
    • 编译后
    • 结论
  • Cython用法之cythonize命令
  • Cython编译引入的类型转换问题
    • 问题描述
    • 排查思路
    • 解决方案
    • 反思

背景

近一年来在Python方面做了不少事情:最早接触Python是利用selenium写了一个网页爬虫。2024年上半年利用scikit-learn做了机器学习方面的入门实践;下半年开始接触UE5、Blender等3D建模、视觉相关工具的脚本开发,这些工具都提供Python的开发接口。通过Python学到了不少新知识,感触最深的就是,Python太方便了。有太多太多的库可以被集成调用,几乎除了业务逻辑,技术实现都可以是拿来主义。

因为之前更多的是项目管理,好久没有发coding的博文了,最近准备把关于Python的相关实践总结下,分一篇或多篇博文记录,主要记录实现方案、遇到的问题和解决思路。
涉及的话题如下,想道哪写到哪。

  1. UE引擎的Python脚本开发,实现批量全自动的渲染队列,并加入通知报警等功能。关键词:UE、ndisplay、渲染队列。
  2. Blender的Python脚本开发,实现自定义的模型生成。关键词:obj文件类型、uv展开、用shapely对二维几何图形的计算、rectpack二维矩形打包问题、pillow图像生成。
  3. Python程序的打包。关键词:PyInstaller、Conda
  4. Python程序的「加密」。关键词:Cython
  5. 更早前的,Python机器学习包。关键词:scikit-learn、pandas、numpy
  6. 更早前的,Python实现的网页爬虫。关键词:selenium

Python程序的「加密」

我们的Python程序是随着客户端一同发布的,为了防止Python源码泄漏,我们通过Cython对核心代码编译后再进行打包发布,以提高窃取/破解的难度或门槛。

什么是Cython

Cython可以将Python程序转化为C/C++语言,并编译为动态库文件(在Windows上为.pyd,在Mac/Linux上为.so),可以支持与C/C++库的调用,可以进一步加速Python程序。因此,Cython本质目的其实并不是做加密,只是经过编译后的Python程序的破解提高了一个难度。

Cython 3.0 中文文档

Cython用法之setup脚本

安装Cython

pip install Cython

编译前

一个简单的例子,目录结构如下:

.
├── hello.py
├── lib
│   └── utils.py
├── main.py
└── setup.py

main.py是主程序,导入了一个hello模块

# main.py
from hello import sayhello

sayhello('Kw')

hello.py也引用了一个/lib/utils.py,用于测试Cython是否会自动检索引用关系并编译引用模块,以及测试编译结果的输出目录结构。

# hello.py
from lib.utils import saywelcome

def sayhello(name):
    print(f'hello {name}')
    saywelcome(name)

/lib/utils.py

# /lib/utils.py
def saywelcome(name):
    print(f'welcome {name}')

setup.py是Cython执行脚本,指定了要编译的hello.py以及lib/utils.py

# setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize([
        "hello.py",
        "lib/utils.py",
    ]), 
)

main.py是入口文件,需要被python解释器调用,因此入口文件是不进行编译的。核心代码应该模块化。

运行setup.py执行编译;其中--inplace令生成的编译文件在当前目录位置:

python3 setup.py build_ext --inplace

编译后

编译完成后的目录结构:

.
├── hello.c
├── hello.cpython-311-darwin.so
├── hello.py
├── lib
│   ├── utils.c
│   └── utils.py
├── main.py
├── setup.py
└── utils.cpython-311-darwin.so

可以看到,在需编译的.py文件同级目录处生成了同名的.c文件,但所有编译后的.so文件均位于构建脚本执行目录,而非源文件所在处。如,utils.xxx.so并没有直接生成在/lib目录下。

为了验证main.py可以正确使用.so以代替.py文件,删除无关的.c/.py文件,并将utils.so移动至/lib下,目录结构如下:

.
├── hello.cpython-311-darwin.so
├── lib
│   └── utils.cpython-311-darwin.so
└── main.py

utils.cpython-311-darwin.so 命名中「cpython-311-darwin」部分无需调整。

执行主程序:python3 main.py,正确输出:

hello Kw
welcome Kw

结论

  1. Cython不会自动检索Python代码的包引用关系,需显示指定每个待编译.py文件。
  2. 输出的.so/.pyd文件不会保持原有目录结构,需自行处理。
  3. 基于结论1和2,需要编写脚本自动编译项目下的所有.py,并保持目录结构。一个思路是先复制整个项目目录文件,用内联的方式在对应位置编译.so/.pyd,以保留目录结构(保留引用关系),最后再删除无用.py/.c(防止源码泄漏)。

Cython用法之cythonize命令

适用于编译单个.py,生成.so。

cythonize -i hello.py

由于前文关于「Cython的结论3」,可以利用该命令编写一个脚本,实现保持工程目录结构的自动编译。见[TODO,还没写]

Cython编译引入的类型转换问题

这个问题其实是撰写本文的初衷

在编译实际Python项目时遇到一个问题:一个模块方法在编译前(.py)源码可以执行,但在编译后(.so)执行异常。提示TypeError: Expected list, got numpy.ndarray

问题描述

问题简化描述为:
/lib/utils.py里有一个将list转化为numpy数组的方法:

# /lib/utils.py
import numpy as np

def list2ndarray(vertices: list):
    vertices = np.array(vertices) 
    # 为何不直接 return np.array(vertices)? 
    # 因为此处是简化的代码,转化后的vertices在实际项目中有其他使用。
    return vertices

main.py中调用时,顺利执行;

# main.py
from lib.utils import list2ndarray

a = list2ndarray([4,5,6])
print(f'{type(a)}, {a}')  # 输出 <class 'numpy.ndarray'>, [4 5 6]

但将(utils.py)编译为utils.so后,执行异常。错误定位在utils.py的这一行代码:

def list2ndarray(vertices: list):
    vertices = np.array(vertices)  # 这一行抛出错误 TypeError: Expected list, got numpy.ndarray
    return vertices

排查思路

  1. 一开始排查方向是numpy的类型转化,通过搜索引擎以及AI查询TypeError: Expected list, got numpy.ndarray相关错误,引导了几个方向:numpy版本问题、输入参数类型问题等。
  2. 通过调试确认输入参数在编译前后没有变化,明确是list类型。代码是在编译后发生异常,可以确认是Cython引入的问题。
  3. 多番尝试(花了一上午),确认是Cython与Python在隐式转换方面的不同导致的。

原因具体是:
def list2ndarray(vertices: list):声明了vertices是list类型,在传入np.array(vertices)后是可以被正确转化的,但返回值是ndarray类型;此时在将该返回值(ndarray类型)赋值给vertices变量(声明为list类型)时,Python完成了隐式转换,而Cython环境下认为非法TypeError: Expected list, got numpy.ndarray:因为对于vertices变量而言,它预期接收list,但却被给予了ndarray。

问题代码换一种写法的话,会使得该问题更清晰:

def list2ndarray(list_value: list): # list_value 是 list类型
    ndarray_value = np.array(list_value) # ndarray_value 是 ndarray类型
    list_value = ndarray_value # Cython下,无法将ndarray_value隐式转化为list_value,因此抛出异常
    return list_value

进一步查询相关资料,得到确认:

Cython的类型声明严格性,而纯Python代码具有动态类型特性,允许隐式转换。

解决方案

确认问题后,解决方案就简单了。
第一种:不声明入参类型,允许隐式转换;但此举难免有点“暧昧不亲”。

def list2ndarray(vertices: list):  # 改为
def list2ndarray(vertices): # 不声明list类型

第二种:避免所谓的“动态类型”,使用正确类型的变量:

def list2ndarray(list_value: list):
    ndarray_value = np.array(list_value)
    # list_value = ndarray_value # 避免所谓的“动态类型”
    return ndarray_value # 使用正确的变量

问题得解。

反思

其实,这个问题就是一个馒头引发的血案,花了一个早上时间排查,值得记录一下。
在Python或JS这类「弱类型、支持动态类型」的编程环境中,没有了类型的束缚,代码写起来非常自由自在,我也很喜欢这种感觉。但是,这就要求Coder对数据的流转了然于心,否则这种小问题很隐晦,可能需要排查半天。反观Java等强类型编程语言里,这种错误编译器都直接提醒你了。

另外,AI确实能提供解决思路,但具体解决方案还是要看人。如果我没有同时做过Java/JS/Python的开发,在这个问题上AI的回答可能让人云里雾里。

相关文章:

  • 深度体验阿里云操作系统控制台
  • Process-based Self-Rewarding Language Models 论文简介
  • 项目管理工具 Maven
  • c语言程序设计--数组里面考察最多的一个知识点-考研冲刺复试面试问答题。
  • 微信小程序引入vant-weapp组件教程
  • 利用后缀表达式构造表达式二叉树的方法
  • [java][RsaUtils]RSA工具类
  • HAL库,配置adc基本流程
  • USB流量分析总结(实战[NISACTF 2022] 破损的flag)
  • 把握好自己的节奏, 别让世界成为你的发条匠
  • Linux 进程间通信
  • 【QT】-易错点笔记-2025-2-7
  • 【组件安装FAQ】Connector 24.10.06-1: [cnc0306002] already exists.
  • 实现自定义圆角imageView, 抗锯齿
  • Django 模型的逆向工程
  • VMware Workstation安装rocky9.5虚拟机
  • 操作系统 2.2-多进程总体实现
  • P8255 [NOI Online 2022 入门组] 数学游戏 题解
  • C++:模板(上篇)
  • 【C++】析构函数与虚析构函数区别
  • 高德vr全景地图下载/代做seo关键词排名
  • 网站开发检测用户微信号/推广app有哪些
  • 宜昌 网站建设 公司/优化步骤
  • 合肥外贸网站建设/推广赚钱的平台有哪些
  • vps云主机可以做网站/产品线上营销推广方案
  • php+mysql网站开发全程实例 于荷云 pdf/网站源码下载