Unicode 和传递字符串

注意

此页面使用两种不同的语法变体

  • Cython 特定的 cdef 语法,旨在使类型声明简洁且易于从 C/C++ 的角度阅读。

  • 纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。

    要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的 cython 模块,例如

    import cython
    

    如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。

与 Python 3 中的字符串语义类似,Cython 严格区分字节字符串和 Unicode 字符串。最重要的是,这意味着默认情况下,字节字符串和 Unicode 字符串之间没有自动转换(除了 Python 2 在字符串操作中所做的)。所有编码和解码都必须通过显式编码/解码步骤进行。为了简化简单情况下的 Python 和 C 字符串之间的转换,可以使用模块级 c_string_typec_string_encoding 指令来隐式插入这些编码/解码步骤。

Cython 代码中的 Python 字符串类型

Cython 支持四种 Python 字符串类型:bytesstrunicodebasestringbytesunicode 类型是普通 Python 2.x 中已知的特定类型(在 Python 3 中分别命名为 bytesstr)。此外,Cython 还支持 bytearray 类型,其行为类似于 bytes 类型,但它是可变的。

str 类型很特殊,因为它在 Python 2 中是字节字符串,在 Python 3 中是 Unicode 字符串(对于使用语言级别 2 编译的 Cython 代码,即默认值)。这意味着它始终与 Python 运行时本身称为 str 的类型完全一致。因此,在 Python 2 中,bytesstr 都表示字节字符串类型,而在 Python 3 中,strunicode 都表示 Python Unicode 字符串类型。切换是在 C 编译时进行的,用于运行 Cython 的 Python 版本无关紧要。

使用语言级别 3 编译 Cython 代码时, str 类型在 Cython 编译时与 Unicode 字符串类型完全一致,即在 Python 2 中运行时它不与 bytes 相一致。

请注意,str 类型与 Python 2 中的 unicode 类型不兼容,即您不能将 Unicode 字符串分配给类型为 str 的变量或参数。尝试这样做会导致编译时错误(如果可检测到)或在运行时抛出 TypeError 异常。因此,在必须与 Python 2 兼容的代码中静态类型化字符串变量时,您应该小心,因为此 Python 版本允许将字节字符串和 Unicode 字符串混合用于数据,并且用户通常期望代码能够处理这两种类型。仅针对 Python 3 的代码可以安全地将变量和参数类型化为 bytesunicode

basestring 类型表示 strunicode 两种类型,即 Python 2 和 Python 3 中的所有 Python 文本字符串类型。这可用于类型化通常包含 Unicode 文本(至少在 Python 3 中)但必须额外接受 Python 2 中的 str 类型以实现向后兼容性的文本变量。它与 bytes 类型不兼容。在正常的 Cython 代码中,它的使用应该很少见,因为通用 object 类型(即无类型代码)通常足够好,并且具有支持分配字符串子类型的额外优势。对 basestring 类型的支持是在 Cython 0.20 中添加的。

字符串字面量

Cython 理解所有 Python 字符串类型前缀

  • b'bytes' 用于字节字符串

  • u'text' 用于 Unicode 字符串

  • f'formatted {value}' 用于 PEP 498(在 Cython 0.24 中添加)中定义的格式化 Unicode 字符串字面量

在使用语言级别 2 编译时,无前缀字符串字面量将变为 str 对象;在使用语言级别 3 编译时,将变为 str 对象(即 unicode)。

关于 C 字符串的一般说明

在许多用例中,C 字符串(也称为字符指针)速度慢且笨拙。一方面,它们通常需要以某种方式进行手动内存管理,这使得代码中更容易引入错误。

然后,Python 字符串对象会缓存它们的长度,因此请求它(例如,为了验证索引访问的边界或将两个字符串连接成一个字符串)是一个有效的常数时间操作。相反,调用 strlen() 从 C 字符串获取此信息需要线性时间,这使得 C 字符串上的许多操作变得相当昂贵。

在文本处理方面,Python 内置支持 Unicode,而 C 则完全缺乏。如果您处理的是 Unicode 文本,通常最好使用 Python Unicode 字符串对象,而不是尝试在 C 字符串中处理编码数据。Cython 使这变得非常容易和高效。

一般来说:除非您知道自己在做什么,否则尽可能避免使用 C 字符串,而应使用 Python 字符串对象。对此的明显例外情况是将它们从外部 C 代码传递到外部 C 代码,反之亦然。此外,C++ 字符串还记得它们的长度,因此在某些情况下,它们可以提供 Python 字节对象的合适替代方案,例如在定义良好的上下文中不需要引用计数时。

传递字节字符串

我们声明了将在本教程中重复使用的虚拟 C 函数

c_func.py
from cython.cimports.libc.stdlib import malloc
from cython.cimports.libc.string import strcpy, strlen

hello_world = cython.declare(cython.p_char, 'hello world')
n = cython.declare(cython.Py_ssize_t, strlen(hello_world))

@cython.cfunc
def c_call_returning_a_c_string() -> cython.p_char:
    c_string: cython.p_char = cython.cast(cython.p_char, malloc(
        (n + 1) * cython.sizeof(cython.char)))

    if not c_string:
        return cython.NULL  # malloc failed

    strcpy(c_string, hello_world)
    return c_string

@cython.cfunc
def get_a_c_string(c_string_ptr: cython.pp_char,
                   length: cython.pointer(cython.Py_ssize_t)) -> cython.int:
    c_string_ptr[0] = cython.cast(cython.p_char, malloc(
        (n + 1) * cython.sizeof(cython.char)))

    if not c_string_ptr[0]:
        return -1  # malloc failed

    strcpy(c_string_ptr[0], hello_world)
    length[0] = n
    return 0

警告

上面/此页面提供的代码通过 cimport (cython.cimports) 使用外部本机(非 Python)库。Cython 编译启用了此功能,但纯 Python 不支持此功能。尝试从 Python(不进行编译)运行此代码将在访问外部库时失败。这在 调用 C 函数 中有更详细的描述。

我们创建了相应的 c_func.pxd 以便能够 cimport 这些函数

c_func.pxd
cdef char* c_call_returning_a_c_string()
cdef int get_a_c_string(char** c_string, Py_ssize_t *length)

在 C 代码和 Python 之间传递字节字符串非常容易。从 C 库接收字节字符串时,您可以让 Cython 将其转换为 Python 字节字符串,只需将其分配给 Python 变量即可

from cython.cimports.c_func import c_call_returning_a_c_string

c_string = cython.declare(cython.p_char, c_call_returning_a_c_string())
if c_string is cython.NULL:
    ...  # handle error

py_string = cython.declare(bytes, c_string)

objectbytes 的类型转换将执行相同的操作

py_string = cython.cast(bytes, c_string)

这将创建一个 Python 字节字符串对象,该对象保存原始 C 字符串的副本。它可以在 Python 代码中安全地传递,并在对它的最后一个引用超出范围时被垃圾回收。重要的是要记住,字符串中的空字节充当终止符,这与 C 中的通用知识一致。因此,以上内容仅适用于不包含空字节的 C 字符串。

除了不适用于空字节之外,以上方法对于长字符串也非常低效,因为 Cython 必须首先在 C 字符串上调用 strlen() 来通过计算到终止空字节的字节数来找出长度。在许多情况下,用户代码已经知道长度,例如因为 C 函数返回了它。在这种情况下,告诉 Cython 确切的字节数以对 C 字符串进行切片会更有效。以下是一个示例

from cython.cimports.libc.stdlib import free
from cython.cimports.c_func import get_a_c_string


def main():
    c_string: cython.p_char = cython.NULL
    length: cython.Py_ssize_t = 0

    # get pointer and length from a C function
    get_a_c_string(cython.address(c_string), cython.address(length))

    try:
        py_bytes_string = c_string[:length]  # Performs a copy of the data
    finally:
        free(c_string)

这里,不需要额外的字节计数,并且 length 个字节将从 c_string 复制到 Python 字节对象中,包括任何空字节。请记住,在这种情况下,切片索引被认为是准确的,并且不会进行边界检查,因此不正确的切片索引会导致数据损坏和崩溃。

请注意,Python 字节字符串的创建可能会因异常而失败,例如由于内存不足。如果您需要在转换后 free() 字符串,则应将赋值包装在 try-finally 结构中

from cython.cimports.libc.stdlib import free
from cython.cimports.c_func import c_call_returning_a_c_string

py_string = cython.declare(bytes)
c_string = cython.declare(cython.p_char, c_call_returning_a_c_string())
try:
    py_string = c_string
finally:
    free(c_string)

要将字节字符串转换回 C char*,请使用相反的赋值

other_c_string = cython.declare(cython.p_char, py_string)  # other_c_string is a 0-terminated string.

这是一个非常快的操作,之后other_c_string指向 Python 字符串本身的字节字符串缓冲区。它与 Python 字符串的生命周期绑定在一起。当 Python 字符串被垃圾回收时,指针将变得无效。因此,只要char*正在使用,就必须保留对 Python 字符串的引用。通常,这仅跨越对接收指针作为参数的 C 函数的调用。但是,当 C 函数存储指针以供日后使用时,必须格外小心。除了保留对字符串对象的 Python 引用之外,不需要手动内存管理。

从 Cython 0.20 开始,bytearray类型得到支持,并且以与bytes类型相同的方式进行强制转换。但是,在 C 上下文中使用它时,必须格外小心,不要在将其转换为 C 字符串指针后扩展或缩小对象缓冲区。这些修改可能会更改内部缓冲区地址,这将使指针无效。

从 Python 代码中接受字符串

另一方面,从 Python 代码接收输入,乍一看可能很简单,因为它只处理对象。但是,在不使 API 过于狭窄或不安全的情况下正确地做到这一点可能并不完全明显。

如果 API 仅处理字节字符串,即二进制数据或编码文本,最好不要将输入参数类型化为类似于bytes的内容,因为这会将允许的输入限制为完全相同的类型,并排除子类型和其他类型的字节容器,例如bytearray对象或内存视图。

根据数据处理的方式(以及位置),最好接收一维内存视图,例如:

def process_byte_data(data: cython.uchar[:]):
    length = data.shape[0]
    first_byte = data[0]
    slice_view = data[1:-1]
    # ...

Cython 的内存视图在Typed Memoryviews中进行了更详细的描述,但上面的示例已经展示了与一维字节视图相关的大部分功能。它们允许对数组进行高效处理,并接受任何可以将自身解包到字节缓冲区的内容,而无需中间复制。处理后的内容最终可以返回到内存视图本身(或其切片),但通常最好将数据复制回一个扁平且简单的bytesbytearray对象,尤其是在仅返回一小部分切片时。由于内存视图不会复制数据,因此它们会使整个原始缓冲区保持活动状态。这里的一般想法是通过接受任何类型的字节缓冲区来放宽输入,但通过返回一个简单且经过良好调整的对象来严格控制输出。这可以通过以下方式简单地完成

def process_byte_data(data: cython.uchar[:]):
    # ... process the data, here, dummy processing.
    return_all: cython.bint = (data[0] == 108)

    if return_all:
        return bytes(data)
    else:
        # example for returning a slice
        return bytes(data[5:7])

对于只读缓冲区,例如bytes,内存视图项类型应声明为const(参见Read-only views)。如果字节输入实际上是编码文本,并且进一步的处理应该在 Unicode 级别进行,那么正确的方法是立即解码输入。这几乎只在 Python 2.x 中是一个问题,在 Python 2.x 中,Python 代码期望它可以将字节字符串(str)与编码文本传递到文本 API 中。由于这通常在模块 API 的多个地方发生,因此辅助函数几乎总是可行的方法,因为它允许轻松地适应以后的输入规范化过程。

这种输入规范化函数通常看起来类似于以下内容

to_unicode.py
from cython.cimports.cpython.version import PY_MAJOR_VERSION

@cython.cfunc
def _text(s) -> str:
    if type(s) is str:
        # Fast path for most common case(s).
        return cython.cast(str, s)

    elif PY_MAJOR_VERSION < 3 and isinstance(s, bytes):
        # Only accept byte strings as text input in Python 2.x, not in Py3.
        return cython.cast(bytes, s).decode('ascii')

    elif isinstance(s, str):
        # We know from the fast path above that 's' can only be a subtype here.
        # An evil cast to <str> might still work in some(!) cases,
        # depending on what the further processing does.  To be safe,
        # we can always create a copy instead.
        return str(s)

    else:
        raise TypeError("Could not convert to str.")
to_unicode.pxd
cdef str _text(s)

然后应该像这样使用它

from cython.cimports.to_unicode import _text

def api_func(s):
    text_input = _text(s)
    # ...

类似地,如果进一步的处理发生在字节级别,但应该接受 Unicode 字符串输入,那么以下方法可能有效,如果你使用的是内存视图

# define a global name for whatever char type is used in the module
char_type = cython.typedef(cython.uchar)

@cython.cfunc
def _chars(s) -> char_type[:]:
    if isinstance(s, str):
        # encode to the specific encoding used inside of the module
        s = cython.cast(str, s).encode('utf8')
    return s

在这种情况下,你可能还想额外确保字节字符串输入确实使用了正确的编码,例如,如果你需要纯 ASCII 输入数据,你可以在循环中遍历缓冲区并检查每个字节的最高位。这应该在输入规范化函数中完成。

处理“const”

许多 C 库在它们的 API 中使用 const 修饰符来声明它们不会修改字符串,或者要求用户不得修改它们返回的字符串,例如

someheader.h
typedef const char specialChar;
int process_string(const char* s);
const unsigned char* look_up_cached_string(const unsigned char* key);

Cython 在语言中支持 const 修饰符,因此你可以直接声明上述函数,如下所示

cdef extern from "someheader.h":
    ctypedef const char specialChar
    int process_string(const char* s)
    const unsigned char* look_up_cached_string(const unsigned char* key)

将字节解码为文本

最初介绍的传递和接收 C 字符串的方式,如果你的代码只处理字符串中的二进制数据,则足够了。但是,当我们处理编码文本时,最佳实践是在接收时将 C 字节字符串解码为 Python Unicode 字符串,并在输出时将 Python Unicode 字符串编码为 C 字节字符串。

对于 Python 字节字符串对象,你通常只需调用 bytes.decode() 方法将其解码为 Unicode 字符串

ustring = byte_string.decode('UTF-8')

Cython 允许你对 C 字符串执行相同的操作,只要它不包含空字节

from cython.cimports.c_func import c_call_returning_a_c_string

some_c_string = cython.declare(cython.p_char, c_call_returning_a_c_string())
ustring = some_c_string.decode('UTF-8')

并且,更有效率的是,对于长度已知的字符串

from cython.cimports.c_func import get_a_c_string

c_string = cython.declare(cython.p_char, cython.NULL)
length = cython.declare(cython.Py_ssize_t, 0)

# get pointer and length from a C function
get_a_c_string(cython.address(c_string), cython.address(length))

ustring = c_string[:length].decode('UTF-8')

当字符串包含空字节时,也应该使用相同的方法,例如,当它使用像 UCS-4 这样的编码时,每个字符都用四个字节编码,其中大部分字节往往是 0。

同样,如果提供了切片索引,则不会进行边界检查,因此不正确的索引会导致数据损坏和崩溃。但是,使用负索引是可能的,并且将在内部注入对 strlen() 的调用,以确定字符串长度。显然,这仅适用于没有内部空字节的以 0 结尾的字符串。以 UTF-8 或其中一个 ISO-8859 编码编码的文本通常是不错的选择。如有疑问,最好传递“明显”正确的索引,而不是依赖数据符合预期。

通常的做法是将字符串转换(以及一般来说非平凡的类型转换)包装在专用函数中,因为这需要在从 C 接收文本时以完全相同的方式完成。这可能看起来像这样

from cython.cimports.libc.stdlib import free

@cython.cfunc
def tounicode(s: cython.p_char) -> str:
    return s.decode('UTF-8', 'strict')

@cython.cfunc
def tounicode_with_length(
        s: cython.p_char, length: cython.size_t) -> str:
    return s[:length].decode('UTF-8', 'strict')

@cython.cfunc
def tounicode_with_length_and_free(
        s: cython.p_char, length: cython.size_t) -> str:
    try:
        return s[:length].decode('UTF-8', 'strict')
    finally:
        free(s)

最有可能的是,你更喜欢在代码中使用更短的函数名,这些函数名基于正在处理的字符串类型。不同类型的內容通常意味着在接收时处理它们的不同方式。为了使代码更易读并预测未来的更改,最佳实践是对不同类型的字符串使用单独的转换函数。

将文本编码为字节

相反的方式,将 Python unicode 字符串转换为 C char*,本身非常有效,假设你真正想要的是内存管理的字节字符串

py_byte_string = py_unicode_string.encode('UTF-8')
c_string = cython.declare(cython.p_char, py_byte_string)

如前所述,这将获取 Python 字节字符串的字节缓冲区的指针。尝试在不保留对 Python 字节字符串的引用的情况下执行相同的操作会导致编译错误

# this will not compile !
c_string = cython.declare(cython.p_char, py_unicode_string.encode('UTF-8'))

在这里,Cython 编译器注意到代码获取了指向临时字符串结果的指针,该结果将在赋值后被垃圾回收。稍后访问无效的指针将读取无效的内存,并可能导致段错误。因此,Cython 将拒绝编译此代码。

C++ 字符串

在包装 C++ 库时,字符串通常以 std::string 类的形式出现。与 C 字符串一样,Python 字节字符串会自动从 C++ 字符串强制转换,反之亦然

# distutils: language = c++

from cython.cimports.libcpp.string import string

def get_bytes():
    py_bytes_object = b'hello world'
    s: string = py_bytes_object

    s.append(b'abc')
    py_bytes_object = s
    return py_bytes_object

C++ 字符串的内存管理机制与 C 不同,因为创建 C++ 字符串会独立复制字符串缓冲区,然后由字符串对象拥有。因此,可以将临时创建的 Python 对象直接转换为 C++ 字符串。常见的应用场景是将 Python Unicode 字符串编码为 C++ 字符串。

cpp_string = cython.declare(string, py_unicode_string.encode('UTF-8'))

需要注意的是,这会带来一些开销,因为它首先将 Unicode 字符串编码为临时创建的 Python 字节对象,然后将其缓冲区复制到新的 C++ 字符串中。

对于反向转换,Cython 0.17 及更高版本提供了高效的解码支持。

# distutils: language = c++

from cython.cimports.libcpp.string import string

def get_ustrings():
    s: string = string(b'abcdefg')

    ustring1 = s.decode('UTF-8')
    ustring2 = s[2:-2].decode('UTF-8')
    return ustring1, ustring2

对于 C++ 字符串,解码切片将始终考虑字符串的正确长度并应用 Python 切片语义(例如,对于超出范围的索引返回空字符串)。

自动编码和解码

自动转换由指令 c_string_typec_string_encoding 控制。它们可以用来改变 C/C++ 字符串强制转换的 Python 字符串类型。默认情况下,它们只从字节类型强制转换,并且必须显式地进行编码或解码,如上所述。

如果所有正在处理的 C 字符串(或绝大多数)包含文本,并且从 Python Unicode 对象自动编码和解码可以稍微减少代码开销,那么这可能不方便。在这种情况下,可以在模块中将 c_string_type 指令设置为 unicode,并将 c_string_encoding 设置为 C 代码使用的编码,例如

# cython: c_string_type=unicode, c_string_encoding=utf8

cdef char* c_string = 'abcdefg'

# implicit decoding:
cdef object py_unicode_object = c_string

# explicit conversion to Python bytes:
py_bytes_object = <bytes>c_string

反向转换,即自动编码为 C 字符串,只支持 ASCII/UTF-8。CPython 在这种情况下通过将编码后的字符串副本与原始 Unicode 字符串一起保留来处理内存管理。否则,将无法以任何合理的方式限制编码字符串的生命周期,从而使从其提取 C 字符串指针的任何尝试都变得危险。以下代码将 Unicode 字符串安全地转换为 UTF-8(将 c_string_encoding 更改为 ASCII 以将其限制为 ASCII)

# cython: c_string_type=unicode, c_string_encoding=UTF8

def func():
    ustring: str = 'abc'
    cdef const char* s = ustring
    return s[0]    # returns 'a' as a Unicode text string

(此示例使用函数上下文来安全地控制 Unicode 字符串的生命周期。全局 Python 变量可以从外部修改,这使得依赖其值的生存期变得危险。)

源代码编码

当字符串字面量出现在代码中时,源代码编码很重要。它决定了 Cython 将为字节字面量存储在 C 代码中的字节序列,以及 Cython 在解析字节编码的源文件时为 Unicode 字面量构建的 Unicode 代码点。遵循 PEP 263,Cython 支持显式声明源文件编码。例如,将以下注释放在 ISO-8859-15(Latin-9)编码的源文件(放在第一行或第二行)的顶部是启用解析器中的 ISO-8859-15 解码所必需的

# -*- coding: ISO-8859-15 -*-

如果没有提供显式编码声明,则源代码将被解析为 UTF-8 编码的文本,如 PEP 3120 所指定。UTF-8 是一种非常常见的编码,它可以表示整个 Unicode 字符集,并且与它高效编码的纯 ASCII 编码文本兼容。这使得它成为源代码文件的非常好的选择,因为源代码文件通常主要由 ASCII 字符组成。

例如,将以下行放在 UTF-8 编码的源文件中将打印 5,因为 UTF-8 将字母 'ö' 编码为两个字节序列 '\xc3\xb6'

print( len(b'abcö') )

而以下 ISO-8859-15 编码的源文件将打印 4,因为该编码只使用 1 个字节来表示这个字母

# -*- coding: ISO-8859-15 -*-
print( len(b'abcö') )

请注意,unicode 字面量 u'abcö' 在两种情况下都是正确解码的四个字符 Unicode 字符串,而没有前缀的 Python str 字面量 'abcö' 在 Python 2 中将成为字节字符串(因此在上面的示例中长度为 4 或 5),而在 Python 3 中将成为 4 个字符的 Unicode 字符串。如果您不熟悉编码,这在初次阅读时可能并不明显。有关详细信息,请参阅 CEP 108

作为经验法则,最好避免使用没有前缀的非 ASCII str 字面量,并对所有文本使用 unicode 字符串字面量。Cython 还支持 __future__ 导入 unicode_literals,它指示解析器将源文件中的所有没有前缀的 str 字面量读取为 unicode 字符串字面量,就像 Python 3 一样。

单个字节和字符

Python C-API 使用普通的 C char 类型来表示字节值,但它有两个特殊的整数类型用于表示 Unicode 代码点值,即单个 Unicode 字符:Py_UNICODEPy_UCS4。Cython 本地支持第一个,对 Py_UCS4 的支持是 Cython 0.15 中的新增功能。 Py_UNICODE 定义为无符号 2 字节或 4 字节整数,或定义为 wchar_t,具体取决于平台。 Py_UCS4 的优点是,它保证足够大,可以容纳任何 Unicode 代码点值,而与平台无关。它定义为 32 位无符号 int 或 long。

在 Cython 中,char 类型在强制转换为 Python 对象时与 Py_UNICODEPy_UCS4 类型不同。类似于 Python 3 中字节类型的行为,char 类型默认情况下强制转换为 Python 整数值,因此以下代码将打印 65 而不是 A

# -*- coding: ASCII -*-

cdef char char_val = 'A'
assert char_val == 65   # ASCII encoded byte value of 'A'
print( char_val )

如果您想要 Python 字节字符串,则必须显式请求它,以下代码将打印 A(或 Python 3 中的 b'A'

print( <bytes>char_val )

显式强制转换适用于任何 C 整数类型。超出 charunsigned char 范围的值将在运行时引发 OverflowError。强制转换也会在赋值给类型化变量时自动发生,例如:

cdef bytes py_byte_string
py_byte_string = char_val

另一方面,Py_UNICODEPy_UCS4 类型很少在 Python Unicode 字符串的上下文之外使用,因此它们的默认行为是强制转换为 Python Unicode 对象。因此,以下代码将打印字符 A,与使用 Py_UNICODE 类型的相同代码相同

cdef Py_UCS4 uchar_val = u'A'
assert uchar_val == 65 # character point value of u'A'
print( uchar_val )

同样,显式强制转换将允许用户覆盖此行为。以下代码将打印 65

cdef Py_UCS4 uchar_val = u'A'
print( <long>uchar_val )

请注意,强制转换为 C long(或 unsigned long)将正常工作,因为 Unicode 字符可以具有的最大码点值为 1114111 (0x10FFFF)。在 32 位或更高平台上,int 也是一样好的。

窄 Unicode 版本

在 3.3 版之前的 CPython 窄 Unicode 版本中,即 sys.maxunicode 为 65535 的版本(例如所有 Windows 版本,与宽版本中的 1114111 相反),仍然可以使用不适合 16 位宽 Py_UNICODE 类型的 Unicode 字符码点。例如,这样的 CPython 版本将接受 Unicode 字面量 u'\U00012345'。但是,在这种情况下,底层系统级编码会泄漏到 Python 空间,因此该字面量的长度变为 2 而不是 1。这在迭代或索引它时也会显示出来。在本例中,可见子字符串为 u'\uD808'u'\uDF45'。它们构成所谓的代理对,代表上述字符。

有关此主题的更多信息,值得阅读 关于 UTF-16 编码的维基百科文章

相同属性适用于为窄 CPython 运行时环境编译的 Cython 代码。在大多数情况下,例如在搜索子字符串时,可以忽略此差异,因为文本和子字符串都将包含代理。因此,大多数 Unicode 处理代码在窄版本上也能正常工作。编码、解码和打印将按预期工作,因此上述字面量在窄和宽 Unicode 平台上都会变成完全相同的字节序列。

但是,程序员应该意识到,单个 Py_UNICODE 值(或 CPython 中的单个“字符”Unicode 字符串)可能不足以在窄平台上表示完整的 Unicode 字符。例如,如果在 Unicode 字符串中独立搜索 u'\uD808'u'\uDF45' 成功,这并不一定意味着字符 u'\U00012345' 是该字符串的一部分。很可能是字符串中存在两个不同的字符,它们恰好与所讨论字符的代理对共享一个代码单元。查找子字符串可以正常工作,因为代理对中的两个代码单元使用不同的值范围,因此该对在代码点序列中始终可识别。

从 0.15 版开始,Cython 扩展了对代理对的支持,因此您可以在窄平台上安全地使用 in 测试来搜索来自完整 Py_UCS4 范围的字符值

cdef Py_UCS4 uchar = 0x12345
print( uchar in some_unicode_string )

类似地,它可以将具有高 Unicode 码点值的单字符字符串强制转换为窄和宽 Unicode 平台上的 Py_UCS4 值

cdef Py_UCS4 uchar = u'\U00012345'
assert uchar == 0x12345

在 CPython 3.3 及更高版本中,Py_UNICODE 类型是系统特定 wchar_t 类型的别名,不再与 Unicode 字符串的内部表示相关联。相反,任何 Unicode 字符都可以在所有平台上表示,而无需使用代理对。这意味着从该版本开始,无论 Py_UNICODE 的大小如何,窄构建都不再存在。有关详细信息,请参阅 PEP 393

Cython 0.16 及更高版本在内部处理此更改,并且对单个字符值也执行正确操作,只要对无类型变量应用类型推断或在源代码中显式使用可移植的 Py_UCS4 类型,而不是平台特定的 Py_UNICODE 类型。Cython 应用于 Python Unicode 类型的优化将像往常一样在 C 编译时自动适应 PEP 393

迭代

Cython 0.13 支持对 char*、字节和 Unicode 字符串的有效迭代,只要循环变量类型正确。因此,以下将生成预期的 C 代码

cdef char* c_string = "Hello to A C-string's world"

cdef char c
for c in c_string[:11]:
    if c == b'A':
        print("Found the letter A")

字节对象也适用

cdef bytes bytes_string = b"hello to A bytes' world"

cdef char c
for c in bytes_string:
    if c == b'A':
        print("Found the letter A")

对于 Unicode 对象,Cython 将自动推断循环变量的类型为 Py_UCS4

cdef unicode ustring = u'Hello world'

# NOTE: no typing required for 'uchar' !
for uchar in ustring:
    if uchar == u'A':
        print("Found the letter A")

自动类型推断通常会导致更有效的代码。但是,请注意,某些 Unicode 操作仍然需要该值为 Python 对象,因此 Cython 最终可能会为循环变量值在循环内部生成冗余的转换代码。如果这会导致特定代码段的性能下降,您可以显式地将循环变量类型化为 Python 对象,或者在循环内部的某个地方将其值分配给 Python 类型变量,以在对它运行 Python 操作之前强制执行一次性强制转换。

还有一些针对 in 测试的优化,因此以下代码将在纯 C 代码中运行(实际上使用 switch 语句)

cpdef void is_in(Py_UCS4 uchar_val):
    if uchar_val in u'abcABCxY':
        print("The character is in the string.")
    else:
        print("The character is not in the string")

结合上面的循环优化,这可以产生非常有效的字符切换代码,例如在 Unicode 解析器中。

Windows 和宽字符 API

警告

强烈建议不要在 Windows 之外使用 Py_UNICODE* 字符串。 Py_UNICODE 本质上在不同平台和 Python 版本之间不可移植。

CPython 3.12 中已删除对 Py_UNICODE C-API 的支持。使用它的代码将无法在最近的 CPython 版本中编译。从 3.3 版本开始,CPython 提供了灵活的 Unicode 字符串内部表示(PEP 393),这使得所有 Py_UNICODE 相关 API 变得过时且效率低下。

Windows 系统 API 本地支持以零终止 UTF-16 编码的 wchar_t* 字符串形式的 Unicode,称为“宽字符串”。

默认情况下,CPython 的 Windows 构建将 Py_UNICODE 定义为 wchar_t 的同义词。这使得内部 unicode 表示与 UTF-16 兼容,并允许进行高效的零拷贝转换。这也意味着 Windows 构建始终是 窄 Unicode 构建,并具有所有注意事项。

为了与 Windows API 互操作,Cython 0.19 支持宽字符串(以 Py_UNICODE* 的形式)并隐式地将它们转换为和从 unicode 字符串对象。这些转换的行为与 char*bytes 的行为相同,如 传递字节字符串 中所述。

除了自动转换之外,出现在 C 上下文中的 Unicode 字面量将变为 C 级别的宽字符串字面量,并且 len() 内置函数专门用于计算以零结尾的 Py_UNICODE* 字符串或数组的长度。

以下是如何在 Windows 上调用 Unicode API 的示例

cdef extern from "Windows.h":

    ctypedef Py_UNICODE WCHAR
    ctypedef const WCHAR* LPCWSTR
    ctypedef void* HWND

    int MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, int uType)

title = u"Windows Interop Demo - Python %d.%d.%d" % sys.version_info[:3]
MessageBoxW(NULL, u"Hello Cython \u263a", title, 0)

CPython 3.3 更改的一个结果是,len()unicode 字符串始终以代码点(“字符”)为单位进行测量,而 Windows API 期望的是 UTF-16 代码单元的数量(其中每个代理都单独计算)。要始终获取代码单元的数量,请直接调用 PyUnicode_GetSize()