Python 3.3:如何访问字符串内部表示?
在Python 3.3及以上版本中,为了帮助解决Unicode编码和解码的问题,我想从Python代码中查看字符串内部实际存储的数据。我该怎么做呢?
有一个叫做str.encode()的方法,它可以返回字节表示,但一般来说,这只是通过特定编码(由“encoding”参数选择)转换后的字节序列,并不是str对象内部实际存储的原始字节。
还有一个“unicode_internal”的编码选项,但这个选项已经被弃用了,而且在3.3版本中,它是否返回真实的内部数据(以什么方式组织?)也不太清楚。
PEP 393描述了Unicode数据的内部结构,似乎要从Python访问这些数据,需要报告字符串的类型(1/2/4字节)、表示方式(ASCII/紧凑型),以及一个包含字符串内容的字节数组(我想它的格式是ASCII、UCS1、2或4)。
我没有找到在str类型上可以提供这种访问的方法。
有没有其他的办法?也许可以巧妙地使用struct?或者有C语言库可以暴露这些字符串的内部结构?
更新于2014年3月13日:
感谢所有提供建议的人,告诉我为什么不应该想要访问字符串的内部结构。这对于普通的Python程序来说确实是有效的建议。
不过,我的问题是:怎么做呢?
进一步解释一下原因:这是为了排查编码解码的问题,可能是某个库中的一个函数创建并返回了一个str,而另一个函数(可能在其他库中)应该对这个str做一些事情。
我想检查这个中间str的确切内容(也就是说,我想把问题空间一分为二),而且不想引入其他变量,比如某个Python函数把数据转换成其他形式(像带转义序列的ASCII)。
还有其他原因,我想知道确切的内部数据,以防某个库对内部数据格式非常敏感。这些库可能是用C语言编写的,可以访问这些数据,并且可能处理不当。
此外,确实应该认为str可以被视为一个代码点的序列,内部表示方式并不重要。但如果字符串处理上真的有bug,我不想被误导;如果没有bug,我希望能确认确实没有。考虑到字符串库的复杂性,零bug将是一个相当大的成就。
所以:我该如何检查字符串的内部结构呢?
3 个回答
为了提高存储unicode值的空间效率,Python进行了内部调整,这个调整是由PEP-393提出的,主要是出于性能考虑。
所以,这个调整对Python代码中从unicode str
值的编码和解码没有任何影响。你完全不需要去关心Python内部是怎么表示这些数据的。比如字符A
,它可能被存储为41
、4100
或41000000
,这取决于字符串中最高的字符需要多少空间,但在ASCII、Latin-1或UTF-8中,它仍然会被编码为41
。
除非你在写一个需要处理这种内部表示的C扩展,否则你完全不需要担心Python是如何存储这些数据的。
如果你想调试编码或解码的问题,可以使用ascii()
函数,这个函数可以将字符串用ASCII码点和Python字符串字面量转义表示,或者你也可以使用ord()
函数,这个函数可以将单个字符转换为对应的整数码点。
对于字节值,可以使用binascii.hexlify()
函数,这个函数可以快速将一系列字节转换为它们的十六进制表示。
在Python中,Unicode字符串可以看作是一串Unicode编码点。它是如何在内部表示的,这与编码和解码的问题没有关系。
你可以通过对字符串中的每个字符使用ord()
函数,来获取这些Unicode编码点的数字值:
>>> list(map(ord, "abc €"))
[97, 98, 99, 32, 8364]
我觉得这对调试编码问题(或者其他任何事情)并没有特别大的帮助,但它可能有助于让你更清楚地理解Unicode字符串的概念。
Python内部的字符串表示方式其实是一个内部实现的细节,可能会因为Python的不同版本或者操作系统而有所不同。因为问题提到的Python版本是3.3及以上,所以我假设我们在讨论的是CPython(1/2/4字节的字符表示),并且使用CPython的一个细节,即id()可以返回内存地址。下面的例子是在Ubuntu 19.10的系统上使用CPython 3.7.5运行的。
from ctypes import string_at
from sys import getsizeof
from binascii import hexlify
a = "ABCDE"
print(hexlify(string_at(id(a), getsizeof(a))))
输出结果:
b'0100000000000000c0988500000000000500000000000000625866dab454b033e
50064016c006d010000000000000000414243444500'
你可以看到在十六进制的末尾有“ABCDE”,从41数到45。如果从Unicode字符范围128-255中添加一个字符,比如说在0xA2的分币符号“¢ABCDE”,这个字符仍然可以用一个字节来表示,所以CPython就这样做了,虽然字符串前面的一排零却莫名其妙地变多了:
b'0200000000000000c09885000000000006000000000000003b7ac7a960368ad4a
4005a006501650200000000000000000000000000000000000000000000000000
00000000000000a2414243444500'
如果添加一个Unicode值超过255的字符,比如在0x153的“œABCDE”,那么整个字符串就会变成每个字符用两个字节来表示,其中“œ”是小端格式的“5301”,“A”是“4100”,依此类推:
b'0200000000000000c0988500000000000600000000000000e50dd134c7e9b87ca
83d22c59341424300000000000000000000000000000000000000000000000000
000000000000005301410042004300440045000000'