Cython中操作C++字符串

使用Cython开发Python扩展模块-CSDN博客中文末对python代码做了部分C++优化,并提及未对字符串类型做优化,并且提到如果不是真正搞懂了C++字符串的操作和Python的C API中与字符串相关的知识,最好不要动Python中的字符串类型。为了搞明白在Cython中如何进行C++字符串操作,恶补了一番std::string的相关知识,同时,在各大AI(助力最大的是Cursor)的指点下,总算有所收获,为了早日忘却,特记录如下。

还是用昨天的中文数字与阿拉伯数字相互转换的程序为例,先看改造后的代码:

python 复制代码
# num_cvt.pyx
# cython: language_level=3
# cython: boundscheck=False
# cython: wraparound=False
# cython: nonecheck=False
# cython: cdivision=True

from libc.stdint cimport int64_t
from libcpp.map cimport map
from libcpp.pair cimport pair 
from libcpp.vector cimport vector
from libc.stddef cimport size_t
from cpython.unicode cimport PyUnicode_AsUTF8String, PyUnicode_Decode

cdef extern from "<string>" namespace "std":
    cdef cppclass string:
        string() except +                              # 默认构造函数
        string(const char*) except +                   # C 字符串构造函数
        string(const char*, size_t) except +           # 带长度的构造函数
        string(const string&) except +                 # 拷贝构造函数
        # 声明我们需要使用的 string 方法
        size_t find(const string&, size_t) const
        string& replace(size_t, size_t, const string&)
        string& erase(size_t, size_t)
        size_t length() const
        string substr(size_t, size_t) const
        const char* c_str()
        bint empty() const
        string& append(const string&)  # 替代 +=
        string& append(const char*)    # 替代 +=
        # 添加运算符声明
        string operator+(const string&) const    # 字符串连接运算符
        bint operator==(const string&) const    # 字符串相等运算符
        bint operator!=(const string&) const    # 字符串不相等运算符
        char operator[](size_t) const           # 索引运算符

# 声明 npos 常量
cdef extern from "<string>" namespace "std::string":
    size_t npos

cdef extern from "<string>" namespace "std":
    string to_string(int64_t val)  # 数字按字面值转 C++ 字符串, 直接用 C++ 库函数

cdef class NumberCvt:
    # 声明 C++ 成员变量
    cdef map[string, int] cn2an_nums      # 中文到数字的映射
    cdef map[int, string] an2cn_nums      # 数字到中文的映射
    cdef map[string, int64_t] cn2an_units     # 中文单位到数字的映射
    cdef vector[string] cn_units          # 中文单位列表

    def __cinit__(self):
        # 初始化数字映射
        cdef dict num_init = {
            "零": 0, "〇": 0, "一": 1, "二": 2, "三": 3,
            "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9
        }
        
        # 初始化 cn2an_nums
        for cn_char, value in num_init.items():
            self.cn2an_nums[self._to_cpp_str(cn_char)] = value
        
        # 初始化 an2cn_nums(不包含〇字)
        for num in range(10):
            for cn_char, val in num_init.items():
                if val == num and cn_char != "〇":
                    self.an2cn_nums[num] = self._to_cpp_str(cn_char)
                    break
        # 初始化单位映射
        cdef dict unit_init = {
            "十": 10,
            "百": 100,
            "千": 1000,
            "万": 10000,
            "亿": 100000000,
            "兆": 1000000000000
        }
        
        # 初始化 cn2an_units
        for unit_char, value in unit_init.items():
            self.cn2an_units[self._to_cpp_str(unit_char)] = value
        
        # 初始化 cn_units
        cdef list units = [
            "", "十", "百", "千", "万", "十", "百", "千",
            "亿", "十", "百", "千", "兆", "十", "百", "千"
        ]
        for unit in units:
            self.cn_units.push_back(self._to_cpp_str(unit))


    cdef object _to_py_str(self, string cpp_str) except *:
        """将 C++ 字符串转换为 Python 字符串"""        
        cdef object py_str = PyUnicode_Decode(cpp_str.c_str(),  # 输入的 C++ 字符串
                    len(cpp_str),     # 字符串长度
                    "utf-8",          # 编码方式
                    "replace"         # 错误处理方式
                ) 
        return py_str

    cdef string _to_cpp_str(self, str py_str) except *:
        """将 Python 字符串转换为 C++ 字符串"""
        cdef bytes py_bytes = PyUnicode_AsUTF8String(py_str)
        cdef string result = string(<const char*>py_bytes, len(py_bytes))
        return result

    cpdef str an2cn(self, int64_t arabic_num):
        cdef string arabic_str = to_string(arabic_num)
        cdef size_t num_len = arabic_str.length()
        if num_len > self.cn_units.size():
            raise ValueError(f"超出数据范围,最长支持 {self.cn_units.size()} 位")

        cdef string output_cn
        cdef int i, d
        cdef char digit
        cdef size_t unit_index
        cdef string last_char
        
        for i in range(num_len):
            digit = arabic_str[i]
            d = ord(digit) - ord('0')  # 将数字字符转换为数字字面量
            # 计算当前数字的单位索引,从位数即可查到对应单位,例如第二位,就是"十",第三位,就是"百",以此类推
            unit_index = num_len - i - 1  
            
            if d:  # 如果当前数字不是0,则直接添加数字和单位
                output_cn += self.an2cn_nums[d] + self.cn_units[unit_index]
            else:  # 如果当前数字是0,则需要特殊处理
                if unit_index % 4 == 0:  # 如果当前单位是万、亿、兆
                    output_cn.append(self._to_cpp_str("零") + self.cn_units[unit_index])
                elif i > 0 and output_cn.length() > 0:  # 如果当前数字是0,且不是第一个数字,则添加"零"
                    last_char = output_cn.substr(output_cn.length() - 3, 3)  # 获取最后一个中文字符
                    if last_char != self._to_cpp_str("零"):
                        output_cn.append(self._to_cpp_str("零"))

        # 处理多余的零和两个万级单位之间全是零等特殊情况
        cdef string result = self._process_special_cases(output_cn)
        return self._to_py_str(result)

    cdef string _process_special_cases(self, string input_str):
        """处理特殊情况的替换,模拟 Python 的 replace 方法"""
        cdef string result = input_str
        cdef map[string, string] replacements
        
        # 初始化替换规则(使用3字节UTF-8编码的中文字符串)
        replacements[self._to_cpp_str("零零")] = self._to_cpp_str("零")
        replacements[self._to_cpp_str("零万")] = self._to_cpp_str("万")
        replacements[self._to_cpp_str("零亿")] = self._to_cpp_str("亿")
        replacements[self._to_cpp_str("亿万")] = self._to_cpp_str("亿")
        replacements[self._to_cpp_str("零兆")] = self._to_cpp_str("兆")
        replacements[self._to_cpp_str("兆亿")] = self._to_cpp_str("兆")
        replacements[self._to_cpp_str("兆万")] = self._to_cpp_str("兆")
        
        # 应用替换规则
        cdef size_t pos
        cdef pair[string, string] replacement_pair
        for replacement_pair in replacements:
            pos = 0
            while True:
                pos = result.find(replacement_pair.first, pos)
                if pos == npos:
                    break
                result.replace(pos, replacement_pair.first.length(), replacement_pair.second)
                pos += replacement_pair.second.length()

        # 处理首尾的零
        cdef string zero = self._to_cpp_str("零")
        while result.length() >= 3 and result.substr(0, 3) == zero:
            result.erase(0, 3)
        while result.length() >= 3 and result.substr(result.length() - 3, 3) == zero:
            result.erase(result.length() - 3, 3)

        # 处理"一十"开头的情况
        cdef string yi_shi = self._to_cpp_str("一十")
        cdef string shi = self._to_cpp_str("十")
        if result.length() >= 6 and result.substr(0, 6) == yi_shi:  # "一十"是6个字节
            result = shi + result.substr(6, npos)

        return result

    cpdef int64_t cn2an(self, str cn_nums):
        cdef int64_t result = 0
        cdef int64_t unit = 1
        cdef int64_t num = 0
        cdef int64_t tmp = 0
        cdef string cpp_cn_nums = self._to_cpp_str(cn_nums)
        cdef size_t i = 0
        cdef string curr_char
        cdef string start_char

        # 处理十、十几的情况
        if cpp_cn_nums.length() == 0:
            raise ValueError(f"没有给出数字。")
        start_char = cpp_cn_nums.substr(0, 3)
        if start_char == self._to_cpp_str("十"):
            if cpp_cn_nums.length() == 3:
                return 10
            else:
                start_char = cpp_cn_nums.substr(3, npos)
                if self.cn2an_nums.find(start_char) != self.cn2an_nums.end(): # 十 + 数字
                    return 10 + self.cn2an_nums[start_char]
                elif self.cn2an_units.find(start_char) != self.cn2an_units.end(): # 十 + 单位
                    return 10 * self.cn2an_units[start_char]
                else:
                    try:
                        curr_char_str = self._to_py_str(start_char)
                    except UnicodeDecodeError:
                        curr_char_str = f"字节值:{curr_char[0]:02x}{curr_char[1]:02x}{curr_char[2]:02x}"
                    raise ValueError(f"'{cn_nums}' 中包含非法字符 '{curr_char_str}'")
        
        # 处理其他情况,从第一个字符开始,逐个字符处理
        while i < cpp_cn_nums.length():
            # 获取一个完整的UTF-8中文字符(3字节)
            curr_char = cpp_cn_nums.substr(i, 3)
            
            # 处理数字
            if self.cn2an_nums.find(curr_char) != self.cn2an_nums.end():
                num = self.cn2an_nums[curr_char]  # 记录数字
            # 处理单位
            elif self.cn2an_units.find(curr_char) != self.cn2an_units.end():
                unit = self.cn2an_units[curr_char]  # 记录单位
                if unit % 10000:  # 单位不是万、亿、兆
                    num *= unit  # 将万以内数值累加到临时结果中
                    tmp += num  
                    num = 0 
                else:  # 单位是万、亿、兆   
                    result += (tmp + num) * unit  # 临时结果乘上单位暂时保存,注意加上最后读到的数字
                    tmp = 0  # 重置临时变量
            else:  # 字符既不在数字列表又不在单位列表中,则是非法字符
                try:
                    curr_char_str = self._to_py_str(curr_char)
                except UnicodeDecodeError:
                    curr_char_str = f"字节值:{curr_char[0]:02x}{curr_char[1]:02x}{curr_char[2]:02x}"
                raise ValueError(f"'{cn_nums}' 中包含非法字符 '{curr_char_str}'")

            i += 3  # 移动到下一个中文字符

        return result + tmp + num  # 返回最终结果,注意加上万以内的数字

下面是对代码的总结:

1、所有用到C++标准库中std::string类型的操作,用到的方法要全部通过cimport导入。一开始因为不知道这一点,明明string可以==,也可以[]取字符,但就是不能通过编译,让我十分疑惑,豆包kimi、copilot一开始都没有提到这一点,直到cursor指出这点,才总算可以继续学习下去了。老实说Cython的这种设计很不友好,后续版本应该改变这一点,导入了类,该类的方法和重载操作符应该就不用再导入而直接使用。

2、C++的string转换成Python的str,似乎是简单的事,结果各大AI都没搞出可用的方法,网上也没有找到可行的方法,最后是综合编译器的报错和各大AI给的示例与错误分析,总算猜到了正确的方法:

cdef object _to_py_str(self, string cpp_str) except *:

"""将 C++ 字符串转换为 Python 字符串"""

cdef object py_str = PyUnicode_Decode(cpp_str.c_str(), # 输入的 C++ 字符串

len(cpp_str), # 字符串长度

"utf-8", # 编码方式

"replace" # 错误处理方式

)

return py_str

3、将Python的str转换为C++的string,各大AI也绕了很多弯子。给出的代码倒都是正确的,但是编译总是出错,问了很久没有一个AI解决了编译错误,最后我问cursor是不是要导入string的构造方法,cursor恍然大悟说您说的对。如果碰上了类似于string是类型而不是函数这样的编译错误,估计就是没有导入用到的C++类的构造函数:

cdef string _to_cpp_str(self, str py_str) except *:

"""将 Python 字符串转换为 C++ 字符串"""

cdef bytes py_bytes = PyUnicode_AsUTF8String(py_str)

cdef string result = string(<const char*>py_bytes, len(py_bytes))

return result

4、int64_t转换为string,其实不需要自己编C++代码,直接导入string to_string(int64_t val)方法即可。需要注意的是,参数数据类型不同的重载方法,只要用到的都要导入。例如,如果还要将int转换为string,就还要导入string to_string(int val)。所以,通过cimport导入方法和类算是Cython编码的一个重要步骤。这个程序光是导入就用了三十多行(当然有些重复的或者学习过程中开始导入了后面又不再使用的,懒得检查了):

from libc.stdint cimport int64_t

from libcpp.map cimport map

from libcpp.pair cimport pair

from libcpp.vector cimport vector

from libc.stddef cimport size_t

from cpython.unicode cimport PyUnicode_AsUTF8String, PyUnicode_Decode

cdef extern from "<string>" namespace "std":

cdef cppclass string:

string() except + # 默认构造函数

string(const char*) except + # C 字符串构造函数

string(const char*, size_t) except + # 带长度的构造函数

string(const string&) except + # 拷贝构造函数

声明我们需要使用的 string 方法

size_t find(const string&, size_t) const

string& replace(size_t, size_t, const string&)

string& erase(size_t, size_t)

size_t length() const

string substr(size_t, size_t) const

const char* c_str()

bint empty() const

string& append(const string&) # 替代 +=

string& append(const char*) # 替代 +=

添加运算符声明

string operator+(const string&) const # 字符串连接运算符

bint operator==(const string&) const # 字符串相等运算符

bint operator!=(const string&) const # 字符串不相等运算符

char operator[](size_t) const # 索引运算符

声明 npos 常量

cdef extern from "<string>" namespace "std::string":

size_t npos

cdef extern from "<string>" namespace "std":

string to_string(int64_t val) # 数字按字面值转 C++ 字符串, 直接用 C++ 库函数

5、这次我用了另一种算法来实现中文数字转换为阿拉伯数字,不倒转原始字符串,直接从最高位开始读,似乎跟人读数字的方式更相似,也更自然。UTF-8字节字符串的遍历方式也很奇葩的,不能将索引每次前进1,要前进3,因为每个UTF-8字符占3字节:

while i < cpp_cn_nums.length():

获取一个完整的UTF-8中文字符(3字节)

curr_char = cpp_cn_nums.substr(i, 3)

省略部分代码

i += 3 # 移动到下一个UTF-8字符,这里是中文

6、std::string具体的方法和属性,可以查文档,这里就不搬运文档了。构建脚本及方法请参阅本文开头提到的博文,完全可以通用,这里就不粘贴了。

7、经过改造,Cython输出的编译注释中黄色底纹已经很少了,而且大多集中在函数定义和异常处理方面:

8、然并卵,改造后程序运行效率还不如没改造字符串类型的版本,比用没改造的原始Python代码构建的框架快不了多少:

可见如果不是频繁进行字符串读写复制,真的没必要修改str数据类型,增加的麻烦太多而取得的效益太少。

相关推荐
鸿业远图科技34 分钟前
分式注记种表达方式arcgis
python·arcgis
别让别人觉得你做不到1 小时前
Python(1) 做一个随机数的游戏
python
巨龙之路2 小时前
C语言中的assert
c语言·开发语言
2301_776681652 小时前
【用「概率思维」重新理解生活】
开发语言·人工智能·自然语言处理
小彭律师3 小时前
人脸识别门禁系统技术文档
python
熊大如如3 小时前
Java 反射
java·开发语言
ll7788113 小时前
C++学习之路,从0到精通的征途:继承
开发语言·数据结构·c++·学习·算法
我不想当小卡拉米4 小时前
【Linux】操作系统入门:冯诺依曼体系结构
linux·开发语言·网络·c++
teacher伟大光荣且正确4 小时前
Qt Creator 配置 Android 编译环境
android·开发语言·qt
炎芯随笔4 小时前
【C++】【设计模式】生产者-消费者模型
开发语言·c++·设计模式