
Python VS Cython
在当今的编程世界里,Python 以其简洁的语法和强劲的库生态系统,成为了快速原型设计和应用开发的首选语言。不过,许多开发者,尤其是那些从事数据科学和机器学习的工程师们,常常会遇到一个令人头疼的问题:Python 的运行速度不够快。对于需要进行大量计算密集型操作,列如矩阵乘法和图像处理等任务,Python 的慢速有时会成为一个瓶颈。
尽管 Python 的内部机制一直在不断演进,通过引入新特性或重写现有功能来提升性能,但其固有的全局解释器锁(Global Interpreter Lock, GIL)在许多时候阻碍了这些努力。当然,也有一些外部库被开发出来,以弥合 Python 与 Java 等编译型语言之间的性能差距。其中,最广为人知的可能是NumPy库,它使用 C 语言实现,旨在支持多核 CPU 和超快的数值及数组处理。另一个超级有效的库是Numba,它利用即时(JIT)编译器,在运行时将部分 Python 和 NumPy 代码转换为快速的机器码,特别适用于加速数值和科学计算任务。
今天,我们要重点介绍一个同样能大幅提升 Python 程序运行速度的强劲工具——Cython。它号称能将你的 Python 代码速度提升惊人的 80 倍。接下来,我将详细为你揭示 Cython 的工作原理,并通过几个实际的例子,手把手教你如何应用它,无论是在 Jupyter Notebook 中还是在常规的 Python 脚本里,都能让你的代码飞驰起来。
Cython:Python 与 C 语言的完美融合
Cython是 Python 语言的一个超集,其设计初衷就是为了让开发者能够用接近 Python 的语法编写出具有 C 语言性能的代码。它的核心工作流程是,将 Python 代码转换成 C 代码,然后再将 C 代码编译成共享库。这些共享库可以像普通的 Python 模块一样被导入和使用,从而让你的程序在享受 C 语言性能优势的同时,依然保持 Python 代码的易读性。
简单来说,Cython 就像一座桥梁,连接了 Python 的开发效率和 C 语言的执行速度。通过它,你可以在 Python 代码中进行类型声明,这些声明让 Cython 编译器能够更高效地将代码转换成优化的 C 代码。这种方法在处理那些性能瓶颈代码时,能带来巨大的性能提升。
Jupyter Notebook 中的 Cython 加速实战
如果你习惯在 Jupyter Notebook 中进行开发,那么使用 Cython 会超级简单。我将通过一个简单的四步流程,来演示如何将一个普通的 Python 函数用 Cython 进行加速。
1. 准备基准测试:编写原生 Python 代码
第一,我们需要一个基准,也就是我们用标准 Python 编写的原始函数,并测量它的运行时间。这将作为我们后续性能提升的参照。
我们来编写一个简单的双重循环函数slow_sum_of_squares,用于计算两个嵌套循环中i*i + j*j的和。这个函数在处理大数据量时会比较耗时,超级适合用来测试性能提升。
# slow_sum_of_squares.py
import timeit
# 定义标准Python函数
def slow_sum_of_squares(n):
total = 0
for i in range(n):
for j in range(n):
total += i * i + j * j
return total
# 对Python函数进行基准测试
print("Python function execution time:")
print("timeit:", timeit.timeit(
lambda: slow_sum_of_squares(20000),
number=1))
在我的高性能系统上,这段代码的执行时间约为 13.13 秒。这是一个相当可观的数字,也为我们接下来的 Cython 优化留下了巨大的空间。
2. 在 Notebook 中启用 Cython 魔法
在 Jupyter Notebook 中,使用 Cython 超级方便。你只需要在 Notebook 的第一个单元格中加载 Cython 扩展,它就像一个魔法开关,开启了 Cython 的功能。
%load_ext Cython
3. 编写 Cython 代码:添加类型声明
接下来,在任何包含你希望用 Cython 运行的 Python 代码的单元格中,添加%%cython魔法命令。这个命令告知 Jupyter,接下来的代码应该由 Cython 进行编译和执行。
Cython 的关键在于类型化。为了获得 C 语言级别的性能,你需要对函数中的参数和变量进行显式类型声明。对于函数参数,你需要在函数定义时直接指定类型,例如def fast_sum_of_squares(int n):。对于函数内部的变量,你需要使用cdef指令来声明它们的类型,例如cdef int total = 0。
经过 Cython 化改造后,我们的求和函数变成了这样:
%%cython
def fast_sum_of_squares(int n):
cdef int total = 0
cdef int i, j
for i in range(n):
for j in range(n):
total += i * i + j * j
return total
import timeit
print("Cython function execution time:")
print("timeit:", timeit.timeit(
lambda: fast_sum_of_squares(20000),
number=1))
在我的系统上,这段 Cython 代码的执行时间仅为 0.158 秒。相比于原始的 Python 代码,这实现了超过 80 倍的惊人加速!这个结果足以证明 Cython 在处理循环等计算密集型任务时的巨大优势。
更多实战案例:从蒙特卡洛到图像处理
为了更全面地展示 Cython 的威力,我们再来看两个更复杂的例子:蒙特卡洛模拟和图像卷积操作。这两个任务都属于计算密集型,是 Cython 大显身手的好地方。
1. 蒙特卡洛模拟计算 π 值
蒙特卡洛模拟是一种利用随机过程来估计系统属性的方法,它需要大量的迭代计算,超级耗费性能。我们用它来计算 π 值。
基本原理是:在一个边长为 1 的正方形内,内切一个半径为 1 的四分之一圆。我们向正方形内随机投掷大量的点,落在四分之一圆内的点数与总点数的比例,会随着点数趋近于无穷大而趋近于 π/4。
下面是原始的 Python 代码,用random.uniform生成随机点:
import random
import time
def monte_carlo_pi(num_samples):
inside_circle = 0
for _ in range(num_samples):
x = random.uniform(0, 1)
y = random.uniform(0, 1)
if (x**2) + (y**2) <= 1:
inside_circle += 1
return (inside_circle / num_samples) * 4
num_samples = 100000000
start_time = time.time()
pi_estimate = monte_carlo_pi(num_samples)
end_time = time.time()
print(f"Estimated Pi (Python): {pi_estimate}")
print(f"Execution Time (Python): {end_time - start_time} seconds")
在我的系统上,执行时间约为 20.67 秒。
目前,我们用 Cython 来优化这段代码。除了添加类型声明外,我们还使用cimport命令从 C 标准库中导入了优化的随机数生成函数rand和RAND_MAX,替代了 Python 的random.uniform(),这进一步提升了性能。
%%cython
import cython
import random
from libc.stdlib cimport rand, RAND_MAX
@cython.boundscheck(False)
@cython.wraparound(False)
def monte_carlo_pi(int num_samples):
cdef int inside_circle = 0
cdef int i
cdef double x, y
for i in range(num_samples):
x = rand() / <double>RAND_MAX
y = rand() / <double>RAND_MAX
if (x**2) + (y**2) <= 1:
inside_circle += 1
return (inside_circle / num_samples) * 4
import time
num_samples = 100000000
# Benchmark the Cython function
start_time = time.time()
pi_estimate = monte_carlo_pi(num_samples)
end_time = time.time()
print(f"Estimated Pi (Cython): {pi_estimate}")
print(f"Execution Time (Cython): {end_time - start_time} seconds")
这段 Cython 代码的执行时间仅为 1.99 秒,实现了约 10 倍的加速。值得一提的是,cimport是 Cython 的一个关键字,专门用于导入 C 语言的函数、变量、常量和类型,这在需要调用底层 C 库时超级有用。
2. 图像卷积实现图像锐化
图像处理中的图像卷积是一种常见的操作,我们用它来给一张稍微模糊的图片进行锐化。
下面是原始的 Python 代码,使用scipy库中的convolve2d函数进行卷积操作。
from PIL import Image
import numpy as np
from scipy.signal import convolve2d
import time
def sharpen_image_color(image):
start_time = time.time()
image = image.convert('RGB')
kernel = np.array([[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]])
image_array = np.array(image)
sharpened_array = np.zeros_like(image_array)
for i in range(3):
channel = image_array[:, :, i]
convolved_channel = convolve2d(channel, kernel, mode='same', boundary='wrap')
convolved_channel = np.clip(convolved_channel, 0, 255)
sharpened_array[:, :, i] = convolved_channel.astype(np.uint8)
sharpened_image = Image.fromarray(sharpened_array)
duration = time.time() - start_time
print(f"Processing time: {duration:.4f} seconds")
return sharpened_image
这段 Python 代码的执行时间约为 0.0317 秒。
目前,我们来看 Cython 的实现。在这个例子中,我们没有使用scipy库,而是直接在 Cython 中用嵌套循环实现了卷积操作。这再次强调了 Cython 在处理底层循环时的优势。我们同样添加了类型声明,并使用了@cython.boundscheck(False)和@cython.wraparound(False)装饰器来禁用边界检查和负索引包装,进一步提升了性能。
%%cython
# cython: language_level=3
# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION
import numpy as np
cimport numpy as np
import cython
@cython.boundscheck(False)
@cython.wraparound(False)
def sharpen_image_cython(np.ndarray[np.uint8_t, ndim=3] image_array):
cdef int kernel[3][3]
kernel[0][0] = 0
kernel[0][1] = -1
kernel[0][2] = 0
kernel[1][0] = -1
kernel[1][1] = 5
kernel[1][2] = -1
kernel[2][0] = 0
kernel[2][1] = -1
kernel[2][2] = 0
cdef int height = image_array.shape[0]
cdef int width = image_array.shape[1]
cdef int channel, i, j, ki, kj
cdef int value
cdef np.ndarray[np.uint8_t, ndim=3] sharpened_array = np.zeros_like(image_array)
for channel in range(3):
for i in range(1, height - 1):
for j in range(1, width - 1):
value = 0
for ki in range(-1, 2):
for kj in range(-1, 2):
value += kernel[ki + 1][kj + 1] * image_array[i + ki, j + kj, channel]
sharpened_array[i, j, channel] = min(max(value, 0), 255)
return sharpened_array
经过 Cython 处理后,这段代码的执行时间仅为 0.0024 秒,比 Python 版本快了近 13 倍。这再次证明了 Cython 在处理图像数据等大规模数组操作时的强劲能力。
告别 Notebook:在常规 Python 脚本中使用 Cython
虽然 Jupyter Notebook 是入门 Cython 的绝佳环境,但大多数实际项目中的 Python 代码都以.py文件的形式存在,并通过命令行运行。在这种情况下,%load_ext和%%cython这些 Notebook 魔法命令就无法使用了。
不过,别担心,在常规 Python 脚本中调用 Cython 也同样简单,只需要多几个步骤。我将以我们第一个sum_of_squares例子为例,来展示这个过程。
1. 创建.pyx文件
第一,你需要将 Cython 代码放入一个独立的.pyx文件中,而不是在 Python 文件中。.pyx是 Cython 的扩展名,它告知编译器这是一个 Cython 文件。
# sum_of_squares.pyx
def fast_sum_of_squares(int n):
cdef int total = 0
cdef int i, j
for i in range(n):
for j in range(n):
total += i * i + j * j
return total
注意,我们移除了%%cython指令和计时代码,由于这些将在主 Python 脚本中处理。
2. 创建setup.py文件进行编译
接下来,你需要创建一个setup.py文件,这个文件负责告知 Python 如何编译你的.pyx文件。
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(
name="cython-test",
ext_modules=cythonize("sum_of_squares.pyx", language_level=3),
py_modules=["sum_of_squares"],
zip_safe=False,
)
这个setup.py文件使用了setuptools和Cython.Build中的cythonize函数。cythonize会找到我们的sum_of_squares.pyx文件,并将其编译成一个 Python 可以导入的 C 扩展模块。
3. 编译.pyx文件
在命令行中,运行以下命令来执行setup.py文件进行编译。
$ python setup.py build_ext --inplace
这个命令会生成一个 C 文件,然后将其编译成一个共享库文件(例如在 Linux 上是.so文件),该文件与你的.pyx文件位于同一目录下。
4. 在主 Python 脚本中调用 Cython 代码
最后,我们创建一个常规的 Python 文件(列如main.py),在这个文件中像导入普通 Python 模块一样导入我们刚刚编译好的 Cython 模块,并调用其中的函数。
# main.py
import time, timeit
from sum_of_squares import fast_sum_of_squares
start = time.time()
result = fast_sum_of_squares(20000)
print("timeit:", timeit.timeit(
lambda: fast_sum_of_squares(20000),
number=1))
运行python main.py,你将看到 Cython 版本代码的执行时间,其结果与在 Jupyter Notebook 中运行时超级接近,同样实现了显著的加速。
总结:性能提升的秘密武器
通过这几个例子,我们充分展示了 Cython 在提升 Python 代码运行速度方面的强劲能力。尽管它在首次使用时可能需要一些额外的学习成本,列如了解类型声明和编译流程,但所带来的性能提升是巨大的。在我们的三个例子中,我们分别实现了 80 倍、10 倍和 13 倍的加速,这足以说明 Cython 的有效性。
Cython的优势在于:
- 性能卓越:它能够将 Python 代码转换为 C 代码,利用 C 语言的执行速度。
- 语法友善:它保留了大部分 Python 的语法,让开发者能够用熟悉的语言编写高性能代码。
- 生态融合:它能与 NumPy 等现有 Python 库无缝集成,甚至可以通过cimport调用 C 标准库,进一步优化性能。
无论你是数据科学家、机器学习工程师,还是任何需要处理计算密集型任务的 Python 开发者,Cython 都值得你深入学习和尝试。通过将代码中的性能瓶颈部分用 Cython 进行重写和编译,你可以轻松地让你的程序运行得更快、更高效,从而将更多的精力投入到解决实际问题上,而不是等待代码运行。
#Python基础#



收藏了,感谢分享