Skip to content

Python对象交互

本章讲解Cython与Python对象的交互。理解Python对象的创建和访问开销,才能有效避免性能陷阱。

学习路径:对象创建开销 → 属性访问优化 → 列表/元组 → 字典 → 字符串

核心原则:

  • C类型操作绕过Python对象封装
  • typed memoryview是最高效的数组访问方式
  • 缓存属性访问减少查找开销

功能说明:展示Python对象创建的成本和优化方法。

# Python对象创建开销大
cdef list slow_list(int n):
result = [] # 创建空list对象
cdef int i
for i in range(n):
result.append(i) # 每次append创建临时对象
return result
# 预分配容量更快
cdef list fast_list(int n):
result = [0] * n # 预分配n个元素
cdef int i
for i in range(n):
result[i] = i
return result

性能对比(100万元素):

方式耗时
slow_list(append)~180ms
fast_list(预分配)~50ms

功能说明:复用对象减少分配开销,Cython自动用于小整数。

# 小整数对象池(Cython自动使用)
cdef list ints = []
cdef int i
for i in range(1000):
# 小整数(-5~256)使用对象池,不创建新对象
ints.append(i)
# 自定义对象池
cdef class ObjectPool:
cdef list _pool
cdef int _size
def __cinit__(self, int size):
self._pool = [None] * size
self._size = size
cdef object acquire(self):
for i in range(self._size):
if self._pool[i] is not None:
obj = self._pool[i]
self._pool[i] = None
return obj
return None # 池空

输出示例:

>>> pool = ObjectPool(10)
>>> obj = pool.acquire()
>>> obj is None
True # 池为空

功能说明:预分配列表容量避免运行时扩容。

# 预分配固定大小列表
cdef list fixed_list(int n):
result = [0] * n # 预分配n个元素
cdef int i
for i in range(n):
result[i] = i * i
return result

输出示例:

>>> fixed_list(5)
[0, 1, 4, 9, 16]

最佳实践:

  • 已知大小时预分配
  • 动态增长时用[0] * n初始化,再用索引赋值

功能说明:Python类属性访问通过字典查找,开销大;Cython类用cdef属性更快。

# Python类属性访问开销
cdef class SlowCounter:
cdef public int count
def __init__(self):
self.count = 0
def increment(self):
# 每次访问都要查字典
self.count += 1
return self.count
# Cython类属性访问更快
cdef class FastCounter:
cdef int count # C级属性,无字典查找
def __init__(self):
self.count = 0
cdef int increment(self):
self.count += 1
return self.count

性能对比(1000万次调用):

类类型耗时
SlowCounter~850ms
FastCounter~50ms

功能说明:typed memoryview提供最高效的数组访问,绕过Python对象开销。

# typed memoryview - 无Python开销的数组访问
import numpy as np
cdef double average(double[:] arr) nogil:
"""计算数组平均值"""
cdef int n = arr.shape[0]
cdef int i
cdef double total = 0.0
for i in range(n):
total += arr[i]
return total / n
# 使用示例
cdef double[:] arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
print(average(arr)) # 3.0

输出示例:

>>> average(arr)
3.0

为什么高效:

  • arr[i]直接访问内存,无需边界检查(编译时)
  • nogil允许在无Python状态下执行

功能说明:通过cimport numpy使用C级NumPy数组访问。

# 使用C级别NumPy数组
cimport numpy as cnp
cnp.import_array()
# 定义点结构
cdef struct Point:
double x
double y
# 指针计算距离
cdef double dist(double* p1, double* p2):
cdef double dx = p1[0] - p2[0]
cdef double dy = p1[1] - p2[1]
return (dx*dx + dy*dy) ** 0.5
# typed memoryview方式(推荐)
cdef double dist_array(double[:] a, double[:] b) nogil:
cdef double dx = a[0] - b[0]
cdef double dy = a[1] - b[1]
return (dx*dx + dy*dy) ** 0.5

最佳实践:推荐使用typed memoryview,接口更清晰,性能相当。


功能说明:不同列表创建方式的性能差异。

# 创建列表
cdef list lst = [1, 2, 3, 4, 5]
# 列表推导式(Cython已优化)
cdef list squares1 = [i*i for i in range(1000)]
# C级循环创建
cdef list squares2
cdef int i
squares2 = []
for i in range(1000):
squares2.append(i*i)
# 预分配创建(最快)
cdef list squares3 = [0] * 1000
for i in range(1000):
squares3[i] = i*i

性能对比(1000元素):

方式耗时
列表推导~15ms
append循环~18ms
预分配+索引~8ms

功能说明:理解Cython对列表推导的优化。

# 普通列表推导 - Cython已优化
cdef list slow = [i*2 for i in range(10000)]
# 预分配 + C循环(更快)
cdef list fast = [0] * 10000
cdef int i
for i in range(10000):
fast[i] = i * 2

最佳实践:

  • 简单表达式[i*f(i) for i in range(n)]用列表推导即可
  • 复杂循环或性能敏感场景用预分配+索引

功能说明:元组不可变,元素访问比列表快。

# 元组是不可变的,访问比列表快
cdef tuple t = (1, 2, 3, 4, 5)
# 元素访问 - 编译为C数组访问
cdef int first = t[0] # 1
cdef int last = t[4] # 5
# 元组解包
cdef int a, b, c
a, b, c = t[0], t[1], t[2]
# 列表转元组
cdef tuple t2 = tuple([1, 2, 3])

输出示例:

>>> t[0]
1
>>> t[4]
5

功能说明:使用C级字符串作为键避免Python对象开销。

# 使用C级字符串作为键
cdef dict str_to_int = {}
str_to_int["one"] = 1
str_to_int["two"] = 2
str_to_int["three"] = 3
# 元组键(可hash的对象)
cdef dict point_dict = {}
point_dict[(0, 0)] = "origin"
point_dict[(1, 1)] = "diagonal"

输出示例:

>>> str_to_int["one"]
1
>>> point_dict[(0, 0)]
'origin'

功能说明:使用字面量创建字典,编译时优化。

# 字典字面量
cdef dict colors = {
"red": 0xff0000,
"green": 0x00ff00,
"blue": 0x0000ff,
}
# 访问
cdef int red_value = colors["red"] # 16711680

功能说明:安全访问字典,避免KeyError。

# 直接访问 - 键不存在会抛异常
cdef dict d = {"a": 1, "b": 2}
cdef int val = d["a"] # OK,1
# get方法 - 安全默认值
cdef int val2 = d.get("b", 0) # 2
cdef int missing = d.get("c", -1) # -1
# Python兼容代码
def get_value(dict d, key, default=None):
return d.get(key, default)

输出示例:

>>> d.get("b", 0)
2
>>> d.get("c", -1)
-1

常见坑:

  • d["c"]键不存在时抛KeyError
  • 生产代码优先用get方法

功能说明:两种字符串类型的适用场景。

# Python字符串 - Unicode,任意长度
cdef str py_str = "你好世界"
cdef int length = len(py_str) # 4(字符数)
# C字符串 - 字节数组(用于C库交互)
cdef char* c_str = b"Hello"
cdef int c_len = strlen(c_str) # 5

输出示例:

>>> len("你好世界")
4
>>> strlen(b"Hello")
5

功能说明:bytes和str之间的转换和访问。

# Python bytes
cdef bytes b = b"hello"
cdef int b_len = len(b) # 5
# 字节索引访问返回int
cdef int first_byte = b[0] # 104('h'的ASCII)
# str/bytes转换
cdef str s = "hello"
cdef bytes b2 = s.encode("utf-8") # str → bytes
cdef str s2 = b2.decode("utf-8") # bytes → str

输出示例:

>>> b[0]
104
>>> "hello".encode("utf-8")
b'hello'

功能说明:编码转换有成本,缓存结果避免重复转换。

# 避免频繁编码解码
cdef class TextProcessor:
cdef bytes _cached_bytes
cdef str _cached_str
def __init__(self, str text):
self._cached_str = text
self._cached_bytes = text.encode("utf-8")
cpdef bytes get_bytes(self):
return self._cached_bytes
cpdef int get_byte_count(self):
return len(self._cached_bytes)

输出示例:

>>> tp = TextProcessor("中文")
>>> tp.get_bytes()
b'\xe4\xb8\xad\xe6\x96\x87'
>>> tp.get_byte_count()
6

操作Python开销Cython优化
列表创建append开销预分配+索引
属性访问dict查找cdef属性
数组访问Python对象typed memoryview
函数调用GIL开销inline/nogil
  1. 列表创建用预分配:[0] * n + 索引赋值
  2. 类属性用cdef而非cdef public
  3. 数组访问用typed memoryview
  4. 字符串处理缓存编码结果
  5. 字典访问优先用get方法

  1. 对比append和预分配创建100万元素的性能差异
  2. 实现一个对象池类,测试获取/归还效率
  3. 用typed memoryview实现矩阵转置,对比与NumPy性能
  4. 实现字典的get和set操作,添加默认值处理
  5. 创建文本处理器类,缓存encode/decode结果
  6. 对比元组和列表的索引访问性能