推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
fy
V2EX  ›  Python

请教, Python3 的 str 底层是用什么编码储存的?

  •  
  •   fy ·
    fy0 · Jan 2, 2016 · 6083 views
    This topic created in 3794 days ago, the information mentioned may be changed or developed.
    感觉 utf32 有时候代价太大,

    utf8 的话,分片不便,可能代价更大……

    没读过源码,求知道的告知一下
    Supplement 1  ·  Jan 3, 2016
    结论汇总在 12 楼,感谢大家,又学到了新姿势。
    19 replies    2016-01-12 19:14:56 +08:00
    qnnnnez
        1
    qnnnnez  
       Jan 2, 2016
    utf8
    lcj2class
        2
    lcj2class  
       Jan 2, 2016   ❤️ 1
    https://docs.python.org/3/howto/unicode.html#the-string-type

    从这里看应该是 utf8 ,但是不确定,做了下面的实验,

    import pickle
    a = "中国人"
    with open("C:/Python33/a.txt", "bw") as f:
    pickle.dump(a, f)

    然后找个能够查看二进制的编辑器,可以看到“中国人”被保存成了 utf-8 的

    e4b8ad ,中
    e59bbd ,国
    e4baba ,人
    fy
        3
    fy  
    OP
       Jan 2, 2016
    @lcj2class WOW ,这个办法好。
    timonwong
        4
    timonwong  
       Jan 2, 2016
    https://docs.python.org/3/howto/unicode.html#the-string-type
    这里描述的是 source encoding (python 源文件默认 utf-8 编码)

    这里是从 python3.3 开始的内部结构(PyUnicodeObject )
    https://www.python.org/dev/peps/pep-0393/
    比较奇葩就是了
    ruoyu0088
        5
    ruoyu0088  
       Jan 2, 2016   ❤️ 1
    pickle 无法 dump 对象内存中的真正的值,可以使用 ctypes 直接访问对象内存:

    import ctypes
    a = "中国人"
    import binascii

    binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))

    输出为:

    b'0200000000000000a073c93ba17f000003000000000000008a3d25ed4d04cf49a86ac83ba17f000000000000000000000000000000000000000000000000000000000000000000002d4efd56ba4e0000'

    最后那部分是保存字符的,每个字符 2 个字节。

    而 a = "abcd"的输出为:

    b'0200000000000000a073c93ba17f00000400000000000000c687538778475d60e57fc83ba17f000000000000000000006162636400'

    每个字符一个字节。
    lcj2class
        6
    lcj2class  
       Jan 2, 2016
    @ruoyu0088

    那这样说的话,就是 UTF-16 存储的了。你能找到源码对应位置吗?我找了半天没找到
    ruoyu0088
        7
    ruoyu0088  
       Jan 2, 2016
    @lcj2class

    不是 UTF-16 储存,是根据字符串的内容自动选择。

    代码在 unicodeobject.c
    congeec
        8
    congeec  
       Jan 2, 2016
    @lcj2class 你这样测的是文件内容的编码,因为极有可能编译器是以 utf-8 编码打开文件的。把文件编码改成 GBK 再试一次看有没有异常
    lcj2class
        9
    lcj2class  
       Jan 2, 2016
    @ruoyu0088
    https://github.com/python/cpython/blob/8a3d7944f8290f095e3c195dd4bafaed9e8e777a/Objects/unicodeobject.c

    这段代码好长,先贴出来,后面慢慢看。

    @congeec
    不是的,我是以二进制的方式写的,和文件的编码没什么关系。
    congeec
        10
    congeec  
       Jan 2, 2016
    @lcj2class 你用 hex 编辑器看看文件内容,用 gbk 和 utf-8 分别写的时候 hex 值是不一样的。我说的不是 a.txt 而是你的源码文件
    lcj2class
        11
    lcj2class  
       Jan 2, 2016
    @congeec
    确实是这样的, 学习了。🙏
    fy
        12
    fy  
    OP
       Jan 3, 2016   ❤️ 1
    我去,原来如此复杂。
    感谢楼上几位给出的资料和方法。

    python 源码中这个文件有 15665 行,实在是让人望而生畏。
    @ruoyu0088 提到编码是自动选择的,我结合读文档的理解和一些测试,
    再次进行了一遍求证,最后得到的结论是:

    python3 的字符串是根据输入确定编码,在 Latin1 , UTF16 、 UTF32 之间进行切换。


    验证的过程是这样的:

    1. 首先是 ucs2(utf16) 的情况 (据我所知 ucs2 与 u16 等价, ucs4 与 u32 等价,不知是否正确)

    In [60]: a = '中文'

    In [61]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
    Out[61]: b'010000003094e51d0200000056f1a772a8000000f447c7000000000000000000020000002d4e87650000'

    In [62]: binascii.hexlify('中文'.encode('utf-16'))
    Out[62]: b'fffe2d4e8765'

    我们可以看到,“中文”两个字在编码为 utf16 之后首先是 fffe 这个头部,随后的 2d4e 8765 分别对应两个字,这与从内存中弄到的字符串形态是相同的。


    2. 那么我们在文本中加入一个 UCS2 表示不了的字符串呢?会怎么样?

    In [64]: a = '\U000a1ffa 中文'

    In [65]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
    Out[65]: b'010000003094e51d03000000220bc0a8b000000000000000000000000000000000000000fa1f0a002d4e00008765000000000000'

    我们可以看到, 2d4e 变成了 2d4e0000 , 8765 变成了 87650000 ,最后是 8 个 0 (一个 UCS4 字符)结尾。

    而 fa1f0a00 是 000a1ffa 在内存中的形式(从右向左,每一个字节——即俩 HEX ——逐个倒装)

    其实我觉得奇怪的地方在于, python 其实记录了文本的长度,为啥坚持 C 风格的字符串(末尾加\0 )?



    看看这个字符串 encode 后的样子吧!

    In [66]: binascii.hexlify('\U000a1ffa 中文'.encode('utf-32'))
    Out[66]: b'fffe0000fa1f0a002d4e000087650000'

    In [67]: binascii.hexlify('\U000a1ffa 中文'.encode('utf-8'))
    Out[67]: b'f2a1bfbae4b8ade69687'

    头部变成了 fffe0000 其他都一致。


    3. 最后再看看单字节的字符串

    In [68]: a = "\x9a\x9b"

    In [69]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
    Out[69]: b'010000003094e51d02000000b9bdd189a4000000000000000000000000000000000000009a9b00'

    =====

    In [70]: a = "\x9a\x9b 中"

    In [71]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
    Out[71]: b'010000003094e51d03000000a4036e4ba8656164f44dc7000000000000000000030000009a009b002d4e0000'

    =====

    In [72]: a = "\x9a\x9b 中\U000a1ffa"

    In [73]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
    Out[73]: b'010000003094e51d04000000776e9375b000005a000000000000000000000000000000009a0000009b0000002d4e0000fa1f0a0000000000'

    果然不出所料。



    说起来\U0000000 是一个比较少用的语法,专门用来转义 UCS4 的。
    要不是前段时间搞了 tinyre 这个项目,我肯定是弄不出这样的字符的……
    顺便宣传一下在下最近这个项目,一个正则引擎: https://github.com/fy0/tinyre
    ruoyu0088
        13
    ruoyu0088  
       Jan 3, 2016
    末尾加\0 是为了和 C 语言兼容,这样可以直接把字符串的地址传递给 C 语言的函数处理。
    pynix
        14
    pynix  
       Jan 3, 2016
    utf8
    pynix
        15
    pynix  
       Jan 3, 2016
    虚拟机层面 3.0 开始使用 Unicode ,后来为了性能优化又改成 utf8 了,好像是 3.3 来着。
    lcj2class
        16
    lcj2class  
       Jan 3, 2016
    之前就发现这里的坑了,之前总结过一次,现在更新了 Python 相关部分,可以参考

    http://liujiacai.net/blog/2015/11/20/strings/#Python
    fy
        17
    fy  
    OP
       Jan 3, 2016
    @ruoyu0088 也是吧,不过也只能管一部分。因为 python 字符串内部可以有\0 。总体来说付出一个字的位置还是值得的。

    但想想还是很蛋疼啊,比如经常有人掺杂一两个诡异的字符在纯英文文本里面,整个文本就会被拉长 2-4 倍不等。

    不过我想最常用的字符集也就是 UCS2 了吧,所以还能够接受的样子。
    fy
        18
    fy  
    OP
       Jan 3, 2016
    对了,其实将\0 当作字符串末尾我觉得不是一个好的设计。

    python 在写扩展的时候,字符串的姿势有好几种,包括这种直接的\0 为末尾,中间不能有\0 的字符串,以及给出长度的字符串等等。
    qnnnnez
        19
    qnnnnez  
       Jan 12, 2016 via iPhone
    看了下代码,发现确实是 UCS1, UCS2, UCS4 三种。之前听许多人说是 utf8 ,就想当然地以为是 utf8 了。现在想想,如果用变长编码,那还会有许多问题。为自己之前的答案道歉。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   878 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 22:05 · PVG 06:05 · LAX 15:05 · JFK 18:05
    ♥ Do have faith in what you're doing.