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_type
和 c_string_encoding
指令来隐式插入这些编码/解码步骤。
Cython 代码中的 Python 字符串类型¶
Cython 支持四种 Python 字符串类型:bytes
、str
、unicode
和 basestring
。 bytes
和 unicode
类型是普通 Python 2.x 中已知的特定类型(在 Python 3 中分别命名为 bytes
和 str
)。此外,Cython 还支持 bytearray
类型,其行为类似于 bytes
类型,但它是可变的。
str
类型很特殊,因为它在 Python 2 中是字节字符串,在 Python 3 中是 Unicode 字符串(对于使用语言级别 2 编译的 Cython 代码,即默认值)。这意味着它始终与 Python 运行时本身称为 str
的类型完全一致。因此,在 Python 2 中,bytes
和 str
都表示字节字符串类型,而在 Python 3 中,str
和 unicode
都表示 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 的代码可以安全地将变量和参数类型化为 bytes
或 unicode
。
basestring
类型表示 str
和 unicode
两种类型,即 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 函数
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 函数 中有更详细的描述。
from libc.stdlib cimport malloc
from libc.string cimport strcpy, strlen
cdef char* hello_world = 'hello world'
cdef size_t n = strlen(hello_world)
cdef char* c_call_returning_a_c_string():
cdef char* c_string = <char *> malloc(
(n + 1) * sizeof(char))
if not c_string:
return NULL # malloc failed
strcpy(c_string, hello_world)
return c_string
cdef int get_a_c_string(char** c_string_ptr,
Py_ssize_t *length):
c_string_ptr[0] = <char *> malloc(
(n + 1) * sizeof(char))
if not c_string_ptr[0]:
return -1 # malloc failed
strcpy(c_string_ptr[0], hello_world)
length[0] = n
return 0
我们创建了相应的 c_func.pxd
以便能够 cimport 这些函数
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)
from c_func cimport c_call_returning_a_c_string
cdef char* c_string = c_call_returning_a_c_string()
if c_string is NULL:
... # handle error
cdef bytes py_string = c_string
对 object
或 bytes
的类型转换将执行相同的操作
py_string = cython.cast(bytes, c_string)
py_string = <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)
from libc.stdlib cimport free
from c_func cimport get_a_c_string
def main():
cdef char* c_string = NULL
cdef Py_ssize_t length = 0
# get pointer and length from a C function
get_a_c_string(&c_string, &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)
from libc.stdlib cimport free
from c_func cimport c_call_returning_a_c_string
cdef bytes py_string
cdef char* c_string = 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.
cdef char* other_c_string = 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]
# ...
def process_byte_data(unsigned char[:] data):
length = data.shape[0]
first_byte = data[0]
slice_view = data[1:-1]
# ...
Cython 的内存视图在Typed Memoryviews中进行了更详细的描述,但上面的示例已经展示了与一维字节视图相关的大部分功能。它们允许对数组进行高效处理,并接受任何可以将自身解包到字节缓冲区的内容,而无需中间复制。处理后的内容最终可以返回到内存视图本身(或其切片),但通常最好将数据复制回一个扁平且简单的bytes
或bytearray
对象,尤其是在仅返回一小部分切片时。由于内存视图不会复制数据,因此它们会使整个原始缓冲区保持活动状态。这里的一般想法是通过接受任何类型的字节缓冲区来放宽输入,但通过返回一个简单且经过良好调整的对象来严格控制输出。这可以通过以下方式简单地完成
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])
def process_byte_data(unsigned char[:] data):
# ... process the data, here, dummy processing.
cdef bint return_all = (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 的多个地方发生,因此辅助函数几乎总是可行的方法,因为它允许轻松地适应以后的输入规范化过程。
这种输入规范化函数通常看起来类似于以下内容
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.")
from cpython.version cimport PY_MAJOR_VERSION
cdef str _text(s):
if type(s) is str:
# Fast path for most common case(s).
return <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 (<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.")
cdef str _text(s)
然后应该像这样使用它
from cython.cimports.to_unicode import _text
def api_func(s):
text_input = _text(s)
# ...
from to_unicode cimport _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
# define a global name for whatever char type is used in the module
ctypedef unsigned char char_type
cdef char_type[:] _chars(s):
if isinstance(s, str):
# encode to the specific encoding used inside of the module
s = (<str>s).encode('utf8')
return s
在这种情况下,你可能还想额外确保字节字符串输入确实使用了正确的编码,例如,如果你需要纯 ASCII 输入数据,你可以在循环中遍历缓冲区并检查每个字节的最高位。这应该在输入规范化函数中完成。
处理“const”¶
许多 C 库在它们的 API 中使用 const
修饰符来声明它们不会修改字符串,或者要求用户不得修改它们返回的字符串,例如
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 c_func cimport c_call_returning_a_c_string
cdef char* some_c_string = 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')
from c_func cimport get_a_c_string
cdef char* c_string = NULL
cdef Py_ssize_t length = 0
# get pointer and length from a C function
get_a_c_string(&c_string, &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)
from libc.stdlib cimport free
cdef str tounicode(char* s):
return s.decode('UTF-8', 'strict')
cdef str tounicode_with_length(
char* s, size_t length):
return s[:length].decode('UTF-8', 'strict')
cdef str tounicode_with_length_and_free(
char* s, size_t length):
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)
py_byte_string = py_unicode_string.encode('UTF-8')
cdef char* c_string = py_byte_string
如前所述,这将获取 Python 字节字符串的字节缓冲区的指针。尝试在不保留对 Python 字节字符串的引用的情况下执行相同的操作会导致编译错误
# this will not compile !
c_string = cython.declare(cython.p_char, py_unicode_string.encode('UTF-8'))
# this will not compile !
cdef char* c_string = 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
# distutils: language = c++
from libcpp.string cimport string
def get_bytes():
py_bytes_object = b'hello world'
cdef string s = 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'))
cdef string cpp_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
# distutils: language = c++
from libcpp.string cimport string
def get_ustrings():
cdef string s = string(b'abcdefg')
ustring1 = s.decode('UTF-8')
ustring2 = s[2:-2].decode('UTF-8')
return ustring1, ustring2
对于 C++ 字符串,解码切片将始终考虑字符串的正确长度并应用 Python 切片语义(例如,对于超出范围的索引返回空字符串)。
自动编码和解码¶
自动转换由指令 c_string_type
和 c_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_UNICODE
和 Py_UCS4
。Cython 本地支持第一个,对 Py_UCS4
的支持是 Cython 0.15 中的新增功能。 Py_UNICODE
定义为无符号 2 字节或 4 字节整数,或定义为 wchar_t
,具体取决于平台。 Py_UCS4
的优点是,它保证足够大,可以容纳任何 Unicode 代码点值,而与平台无关。它定义为 32 位无符号 int 或 long。
在 Cython 中,char
类型在强制转换为 Python 对象时与 Py_UNICODE
和 Py_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 整数类型。超出 char
或 unsigned char
范围的值将在运行时引发 OverflowError
。强制转换也会在赋值给类型化变量时自动发生,例如:
cdef bytes py_byte_string
py_byte_string = char_val
另一方面,Py_UNICODE
和 Py_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()
。