python如何编译py文件生成pycpyopyd以及如何和C语言结合使用

Posted xiondun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python如何编译py文件生成pycpyopyd以及如何和C语言结合使用相关的知识,希望对你有一定的参考价值。

python如何编译py文件生成pyc、pyo、pyd以及如何和C语言结合使用


喜欢这篇文章的话,就去bilibili看看我吧,虽然啥也没有。:https://space.bilibili.com/12921175


python执行py文件的流程

当我们执行一个py文件的时候,直接python xx.py即可,那么这个流程是怎么样的呢。先说明一下,python执行代码实际上是先打开文件然后执行里面的代码,所以文件的扩展名不一定是py的形式,txt形式也是依旧可以成功执行,只要文件里面的代码是符合python规范的。下面我们来看看python是怎么执行py文件的。

  • 先将文件里面的内容读取出来,scanner对其进行扫描,切分成一个个的token
  • parser对token进行解析,建立抽象语法树(AST,abstract syntax tree)
  • compiler对ast进行编译,得到python字节码
  • code evaluator执行字节码

我们注意到第三个过程,是一个编译的过程。说明python即便是解释性语言,也依旧存在着编译的过程,这一点和java是一样的。之所以要存在编译的过程,主要是为了优化执行的速度,比如元组,或者函数里面出现了yield,这一点在编译的时候就已经确定了,编译的时候就已经知道这是一个什么样的数据结构,那么在执行的时候可以很快速的分配相应的内存。我们在打开python文件所在的目录的时候,总会看到一个__pycache__的文件夹,这里面存放的就是python编译之后的字节码。当执行python文件的时候,会检测当前的__pycache__目录中是否有对应的字节码,没有就创建,有的话比较字节码的创建时间和当前py文件的修改时间,如果字节码的创建时间要晚一些,说明用户没有修改文件,于是执行字节码,如果字节码的创建时间要早一些,说明用户修改了python源代码,那么就会从新编译得到一个新的字节码。此外编译还有一个重要的特点,就是语法检测。错误分为两种:一种是语法错误,另一种是逻辑错误。

  • 语法错误就是源代码没有遵循python的规范,比如if判断使用了一个=,或者for循环后面没有:等等,这些都是属于语法错误,这是一种低级的错误,在编译的时候就会失败。

     
    try:
        >
    except Exception:
        pass
    """
    这个代码是编译不过去的,即便你使用了try···except。
    语法错误就是不遵循python规范,编译的时候都编译不过。
    """
    
  • 那么另一种错误就是逻辑错误,这是语法没问题,但是执行的时候出错了,比如索引越界、和0相除、变量没有定义等等,这些错误是在运行的时候才会出现的,这是可以被捕获的。

     
    try:
        a
    except Exception:
        pass
    # 这段代码是不会报错的。
    

python如何编译py文件生成字节码

python中的字节码有两种,pyc和pyo,两者本质上没啥区别,只不过pyo的优化程度更高一些。

编译可以通过py_compile模块进行编译

 
# test.py
deffoo(name):
    print("hello " + name)

我们来对test.py进行编译

 
import py_compile

"""
参数如下:
file:要编译的py文件
cfile:编译之后的字节码文件,不指定的话默认为源文件目录下的__pycache__目录的下的\'源文件名.解释器类型-python版本.字节码类型\'文件
dfile:错误消息文件,默认和cfile一样,一般不用管
doraise:是否开启异常处理,默认和False
optimize:优化字节码级别。如果是pyc:可以选-1或0。pyo的话,可以选1或2。都是值越小优化程度越高
"""
py_compile.compile(file="test.py", cfile=r"./test.pyc", optimize=-1)
py_compile.compile(file="test.py", cfile=r"./test.pyo", optimize=1)

可以看到,已经编译成功了,pyc是可以直接当做普通py文件导入的,但是pyo貌似不可以,所以一般我们只编译成pyc形式的字节码。但是如果不导入只是执行的话,那么是可以编译成pyo的。

 
import test
test.foo("mashiro")  # hello mashiro

编译的另一种方式,我们也可以直接使用命令行。

 
编译成pyc
python -m py_compile 源代码

编译成pyo
python -O -m py_compile 源代码

如果需要编译整个目录内的所有源代码
python compileall

编译成pyd文件

这个pyd实际上就是Windows上的dll文件,但是pyd是由py文件生成的,是可以直接当成python模块导入的。而dll的话一般是c或者c++编写的扩展模块,这个时候我们会使用ctypes进行加载,后面会说。而Windows的pyd在linux上面则是so文件,dll在linux上面也是so文件,这个时候是使用ctypes还是使用普通加载模块的方式,就看具体情况了。

我们下面测试一段python代码,看看会用多长时间,然后将其编译成pyd之后再测试一下。

 
# test_v.py
deffunc():
    for _ in range(10000):
        sum = 0
        for i in range(100000):
            sum += i

可以看到我们将sum依次从0加到100000-1,然后重复这个过程10000次,我们来测试一下用了多长时间。

 
import time
from test_v import func

start = time.perf_counter()
func()
end = time.perf_counter()

print("总耗时:", end - start)  # 总耗时: 45.554086

直接导入py文件,调用函数执行,总共花了45秒钟,下面我们来编译成pyd。

那么如何编译成pyd呢?

首先确保电脑上安装了64位的MinGW,然后安装cython,pip install cython,新建一个py文件to_pyd.py

 
# to_pyd.py
# 导入模块
import Cython.Build

# 传入要编译成pyd的py文件
ext = Cython.Build.cythonize("test_v.py")

# 下面还要导入另一个模块
import distutils.core

# 调用setup方法
distutils.core.setup(
    ext_modules=ext,  # 将Cython.Build.cythonize返回的结果传进去
)

然后在命令行输入python to_pyd.py build,即可把py文件test_v.py编译成pyd。执行之后,会得到一个对应的test_v.c文件,以及一个build目录。这个生成的c文件我们不需要管,我们看看build目录。

我们看到此时就得到了对应的pyd文件,也叫test_v,后面的则是python的版本号以及操作系统类型、位数等等,我们来测试一下性能吧。只把那个pyd文件拿出来,其他没用的都删掉,

 
import time
import test_v

# 我们看到导入之后,显示的是pyd
print(test_v)  # <module \'test_v\' from \'C:\\\\Users\\\\satori\\\\Desktop\\\\love_minami\\\\test_v.cp38-win_amd64.pyd\'>

start = time.perf_counter()
test_v.func()
end = time.perf_counter()

print("总耗时:", end - start)  # 总耗时: 12.3021872

此时我们惊奇地看到,用了12秒,确实快了不少。主要是cython将python代码进行了优化,另外编译成pyd之后,是很难再反编译成py文件的,如果你的模块必须开源但是又不想被人看到某些细节的话,那么就可以编译成pyd。对于字节码pyc文件的反编译已经有人实现了,可以将pyc转成py文件,但是pyd目前还没有被反编译过。

那为什么编译成pyd的时候速度会提升呢?主要是cython将代码进行了优化,转化成了c一级的代码。另外我们说,test_v.cp38-win_amd64.pyd里面的38就是解释器的版本,我们这里是python3.8。这样的话,也就意味着只有当你的版本是python3.8的时候,才会去导入这个模块,于是我们把中间那一串给删掉只保留test_v.pyd可不可以呢?我们可以试一下

 
import test_v

print(test_v)  # <module \'test_v\' from \'C:\\\\Users\\\\satori\\\\Desktop\\\\love_minami\\\\test_v.pyd\'>

事实证明确实是可以的,另外这样的话不光是python3.8,其他版本的python也是可以导入的,只要编译成pyd所使用的py文件,符合执行的python解释器的语法规范即可。

python结合c语言

我们说使用cython确实能够加速代码,但肯定还是没有原生的c语言执行的快。我们将上面的代码转换成c的代码来测试一下,进而引入如何将python和c进行结合。

 
//1.c
longlongfunc(){
    int _;
    long long sum;
    long i;

    for (_ = 0;_ < 10000; _ ++)
    {
        sum = 0;
        for (i = 0;i < 100000; i++)
        {
            sum += i;
        }
    }
    return sum;
}

然后我们将这个1.c编译成dll,在linux中就是so,通过命令gcc -o 编译之后的dll或者so文件名 -shared c源文件编译。

我们这里就编译成mmp.dll吧:所以是gcc -o mmp.dll -shared 1.c

可以看到mmp.dll已经出现了, 下面就来调用它

 
import time
import ctypes

# 调用ctypes.cdll.LoadLibrary,传入dll的路径
# 这个方法就等价于dll = ctypes.CDLL("xxx.dll"),用哪种都行,但是要求dll或者so的路径是绝对路径
# 另外这两种方式在Windows上加载dll和linux上加载so都是可以的。
dll = ctypes.cdll.LoadLibrary(r"C:\\Users\\satori\\Desktop\\love_minami\\mmp.dll")

start = time.perf_counter()
# 此时把dll看成一个模块即可,里面定义了很多函数,比如func
dll.func()
end = time.perf_counter()
print("总耗时:", end - start)  # 总耗时: 2.3377831

可以看到用时不到3秒,而我使用原生的python执行需要45秒,使用cython加速也需要12秒。首先我必须指出,当sum依次从0加到100000-1时,long long存不下。但是相同功能的程序,c的速度肯定会比cython编译的pyd快,这一点可以自己测试一下,我这里就不再试了。

ctypes类型和c语言类型

我们直接调用一个函数显然是没有问题的,但如果函数里面需要参数呢?我们还能直接传递python的原生类型吗?

 
//计算两个数之和
intadd(int a, long b){
    int sum;
    sum = a + b;
    return sum;
}

//查找指定字符在字符串中出现的位置
intfind_pos(char *string, char subchar){
    char *p;
    int pos = 0;
    for (p = string; *p != \'\\0\'; p++, pos++){
        if (*p == subchar)
        {
            return pos;
        }
    }
    return -1;
}
 
import ctypes

dll = ctypes.cdll.LoadLibrary(r"C:\\Users\\satori\\Desktop\\love_minami\\mmp.dll")

print(dll.add(100, 200))  # 300
print(dll.find_pos("satori", "a"))  # -1

我们看到对于整型来说是没有问题的,但是对于字符串就有问题了,因为c中没有字符串的概念,这时候应该怎么做呢?

 
import ctypes
from ctypes import c_char_p, c_char

dll = ctypes.cdll.LoadLibrary(r"C:\\Users\\satori\\Desktop\\love_minami\\mmp.dll")

# c语言中没有字符串这个概念,c语言中的字符串实际上是字符数组,c的这些概念不再介绍
# 传递一个指向字符数组的指针,同理字符a不能直接传递,需要使用c_char包装一下,并且里面需要传递字节。
print(dll.find_pos(c_char_p(b"satori"), c_char(b"a")))  # 1

所以我们来看看ctypes给我们提供了哪些类型,这些类型又对应c中的哪些类型呢?

 
from ctypes import *

print(c_int(1))  # c_long(1)
print(c_uint(1))  # c_ulong(1)
print(c_short(1))  # c_short(1)
print(c_ushort(1))  # c_ushort(1)
print(c_long(1))  # c_long(1)
print(c_ulong(1))  # c_ulong(1)
print(c_longlong(1))  # c_longlong(1)
print(c_ulonglong(1))  # c_ulonglong(1)
print(c_float(1.1))  # c_float(1.100000023841858)
print(c_double(1.1))  # c_double(1.1)

# 在64位机器上,c_longdouble等于c_double
print(c_longdouble(1.1))  # c_double(1.1)

print(c_bool(True))  # c_bool(True)

# 必须传递一个字节或者只有一个元素的字符数组,或者一个int
# 代表c里面的字符
print(c_char(b"a"), c_char(bytearray(b"x")))  # c_char(b\'a\') c_char(b\'x\')

# 传递一个unicode字符
print(c_wchar("憨"))  # c_wchar(\'憨\')

# 和c_char类似,但是要求传递一个整型
print(c_byte(97))  # c_byte(97)
print(c_ubyte(97))  # c_ubyte(97)

# c_char_p就是c里面字符数组指针了
# char *s = "hello world";
# 那么这里面也要传递一个字符数组,字符是bytes类型,返回一个地址
print(c_char_p(b"hello world"))  # c_char_p(2082736374464)

# 直接传递一个unicode,同样返回一个地址
print(c_wchar_p("憨八嘎~"))  # c_wchar_p(2884583039392)

# 并且还有一个c_size_t和c_ssize_t
# 相当于c_ulonglong和c_longlong,这个和机器有关
print(c_size_t(10))  # c_ulonglong(10)
print(c_ssize_t(10))  # c_longlong(10)

当然c中各种类型,在ctypes都有对应。比如我们没有介绍的结构体等等,更复杂的用法可以参考官网。

 
 

以上是关于python如何编译py文件生成pycpyopyd以及如何和C语言结合使用的主要内容,如果未能解决你的问题,请参考以下文章

Python - 浅谈Python的编译与反编译

python_1 python的编译过程

python手动编译py_compile,compileall

逆向工程:Python反编译工具的一点介绍(如何还原pyc文件)

逆向工程:Python反编译工具的一点介绍(如何还原pyc文件)

逆向工程:Python反编译工具的一点介绍(如何还原pyc文件)