likes
comments
collection
share

【从1到∞精通Python】16、unicode,py3的字符串实现

作者站长头像
站长
· 阅读数 24

原文链接:【Hard Python】【第五章-字符串】1、unicode,py3的字符串实现

python的字符串实质到底是什么类型的数据,这个可是困扰着很多编程者的话题。在python2我们已经被中文编码相关的问题折磨的不轻,那到了python3之后为什么又解决了这个问题呢?今天这篇文章就带大家详细剖析python3的字符串实现。

我们首先看一段代码:

def test_str_basic():
    s = '123456789'
    print(type(s))

这段代码打印了一个字符串对象的类型,其结果为<class 'str'>str类型从哪里来?从C源码中我们可以搜索到,其来源于unicodeobject.cPyUnicode_Type

// unicodeobject.c
PyTypeObject PyUnicode_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "str",                        /* tp_name */
    sizeof(PyUnicodeObject),      /* tp_basicsize */
    0,                            /* tp_itemsize */
    /* Slots */
    (destructor)unicode_dealloc,  /* tp_dealloc */
    0,                            /* tp_vectorcall_offset */
    0,                            /* tp_getattr */
    0,                            /* tp_setattr */
    0,                            /* tp_as_async */
    unicode_repr,                 /* tp_repr */
    &unicode_as_number,           /* tp_as_number */
    &unicode_as_sequence,         /* tp_as_sequence */
    &unicode_as_mapping,          /* tp_as_mapping */
    (hashfunc) unicode_hash,      /* tp_hash*/
    0,                            /* tp_call*/
    (reprfunc) unicode_str,       /* tp_str */
    PyObject_GenericGetAttr,      /* tp_getattro */
    0,                            /* tp_setattro */
    0,                            /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
        Py_TPFLAGS_UNICODE_SUBCLASS |
        _Py_TPFLAGS_MATCH_SELF, /* tp_flags */
    unicode_doc,                  /* tp_doc */
    0,                            /* tp_traverse */
    0,                            /* tp_clear */
    PyUnicode_RichCompare,        /* tp_richcompare */
    0,                            /* tp_weaklistoffset */
    unicode_iter,                 /* tp_iter */
    0,                            /* tp_iternext */
    unicode_methods,              /* tp_methods */
    0,                            /* tp_members */
    0,                            /* tp_getset */
    &PyBaseObject_Type,           /* tp_base */
    0,                            /* tp_dict */
    0,                            /* tp_descr_get */
    0,                            /* tp_descr_set */
    0,                            /* tp_dictoffset */
    0,                            /* tp_init */
    0,                            /* tp_alloc */
    unicode_new,                  /* tp_new */
    PyObject_Del,                 /* tp_free */
};

对应地,其大小为PyUnicodeObject所占的大小。PyUnicodeObject可表示的数据结构如下:

// unicodeobject.h
typedef struct {
    PyCompactUnicodeObject _base;
    union {
        void *any;
        Py_UCS1 *latin1;
        Py_UCS2 *ucs2;
        Py_UCS4 *ucs4;
    } data;                     /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;


typedef struct {
    PyASCIIObject _base;
    Py_ssize_t utf8_length;     /* Number of bytes in utf8, excluding the
                                 * terminating \0. */
    char *utf8;                 /* UTF-8 representation (null-terminated) */
    Py_ssize_t wstr_length;     /* Number of code points in wstr, possible
                                 * surrogates count as two code points. */
} PyCompactUnicodeObject;


typedef struct {
    PyObject_HEAD
    Py_ssize_t length;          /* Number of code points in the string */
    Py_hash_t hash;             /* Hash value; -1 if not set */
    struct {
        unsigned int interned:2;
        unsigned int kind:3;
        unsigned int compact:1;
        unsigned int ascii:1;
        unsigned int ready:1;
        unsigned int :24;
    } state;
    wchar_t *wstr;              /* wchar_t representation (null-terminated) */
} PyASCIIObject;

其中,PyUnicodeObjectPyCompactUnicodeObjectPyASCIIObject分别对应三种不同的字符串类型,它们有以下区别:

  • PyASCIIObject:通过PyUnicode_New生成的只包含ASCII字符的字符串,字符数据会紧随结构体排列
  • PyCompactUnicodeObject:通过PyUnicode_New生成的包含非ASCII字符的字符串,字符数据会紧随结构体排列
  • PyUnicodeObject:通过PyUnicode_FromUnicode创建

其中PyUnicode_FromUnicode是已经过时的方法,因此我们常见的unicode对象是以PyASCIIObjectPyCompactUnicodeObject的形式存在的,也就是说现在我们日常创建的字符串对象,其数据会紧随结构体排列。

pythonunicode实现是对标标准unicode规范的,因此要深入了解其中原理,我们需要预备一些关于unicode的知识,比如:

unicode本身存在的意义是将世界上任意一个字符(包括emoji)映射到一个特定的数字,这个数字被称为code pointunicodecode point是分组的,每组65536个,称作为一个个plane。每个unicode字符用4个字节表示,但如果需要进行二进制编码的话,比如存储文件或是网络传输,每个字符都使用4个字节往往会有冗余,因此需要一个比较效率的二进制编码方式,比如:utf8utf16

utf8编码是最常见的编码之一,其长度可变,但针对不同unicodecode point值,会编码成不同长度的形式,比如ASCII支持的英文在utf8编码里只占1个字节,但汉字可能就是3个字节。在python中,当我们需要把字符串编码成utf8的二进制形式时,就需要string.encode('utf-8')的调用,反之假设我们从一个socket接收到utf8编码的数据,需要解码成字符串时,就需要bytestring.decode('utf-8')对字符串进行解码。

言归正传,现在我们回到对PyASCIIObjectPyCompactUnicodeObject数据结构的研究当中。首先我们看PyASCIIObject,它有以下几个部分:

  • length:字符串的长度,按code point个数计算
  • hash:字符串的hash编码
  • state:字符串状态,用一个完整4字节存储
    • interned(2):是否被短字符串缓存,以及是否永久缓存
    • kind(3):字符串的类型
      • 0:wide-char宽字符类型
      • 1:1byte,全部字符都可用8bits无符号表示
      • 2:2byte,全部字符都可用16bits无符号表示
      • 4:4byte,全部字符都可用32bits无符号表示
    • compact(1):字符串数据是否紧凑于结构体排列
      • 先前提到PyASCIIObjectPyCompactUnicodeObject都是紧凑排列
    • ascii(1):是否只有ascii字符
      • 先前kind=1的时候由于是unsigned,因此可以表示除ascii外到200多的字符
    • ready(1):是否数据已准备完成
      • 紧凑排列数据,或者非紧凑排列但数据指针已经填好字符串数据,都算ready
    • 24位padding
  • wstrwide-char的字符串表示

PyASCIIObject用来表示ASCII字符,而含有非ASCII字符的字符串则用PyCompactUnicodeObject表示,其包含以下内容:

  • _basePyASCIIObject的实例
  • utf8_length:除了\0之外,utf8的字符串表示的比特数
  • utf8utf8的字符串表示
  • wstr_length:wide-char字符串表示里code point的个数

而字符串的真实数据则放到了PyUnicodeObjectdata当中,以一个union的形式表示不同长度表示的字符串

接下来我们通过一个例子来展示unicode字符串是如何被创建的。我们的代码是字母+汉字+数字:

s = "abc哈咯123"

当这段代码被打入到解释器中,被词法分析器分析时,就会调用字符串创建的逻辑unicode_decode_utf8,将const char类型的原生字符转化为PyUnicodeObject

// unicodeobject.c
static PyObject *
unicode_decode_utf8(const char *s, Py_ssize_t size,
                    _Py_error_handler error_handler, const char *errors,
                    Py_ssize_t *consumed)
{
    if (size == 0) {
        if (consumed)
            *consumed = 0;
        _Py_RETURN_UNICODE_EMPTY();
    }

    /* ASCII is equivalent to the first 128 ordinals in Unicode. */
    if (size == 1 && (unsigned char)s[0] < 128) {
        if (consumed) {
            *consumed = 1;
        }
        return get_latin1_char((unsigned char)s[0]);
    }

    const char *starts = s;
    const char *end = s + size;

    // fast path: try ASCII string.
    PyObject *u = PyUnicode_New(size, 127);
    if (u == NULL) {
        return NULL;
    }
    s += ascii_decode(s, end, PyUnicode_1BYTE_DATA(u));
    if (s == end) {
        return u;
    }

    // Use _PyUnicodeWriter after fast path is failed.
    _PyUnicodeWriter writer;
    _PyUnicodeWriter_InitWithBuffer(&writer, u);
    writer.pos = s - starts;

    Py_ssize_t startinpos, endinpos;
    const char *errmsg = "";
    PyObject *error_handler_obj = NULL;
    PyObject *exc = NULL;

    while (s < end) {
        Py_UCS4 ch;
        int kind = writer.kind;

        if (kind == PyUnicode_1BYTE_KIND) {
            if (PyUnicode_IS_ASCII(writer.buffer))
                ch = asciilib_utf8_decode(&s, end, writer.data, &writer.pos);
            else
                ch = ucs1lib_utf8_decode(&s, end, writer.data, &writer.pos);
        } else if (kind == PyUnicode_2BYTE_KIND) {
            ch = ucs2lib_utf8_decode(&s, end, writer.data, &writer.pos);
        } else {
            assert(kind == PyUnicode_4BYTE_KIND);
            ch = ucs4lib_utf8_decode(&s, end, writer.data, &writer.pos);
        }

        switch (ch) {
        case 0:
            if (s == end || consumed)
                goto End;
            errmsg = "unexpected end of data";
            startinpos = s - starts;
            endinpos = end - starts;
            break;
        case 1:
            errmsg = "invalid start byte";
            startinpos = s - starts;
            endinpos = startinpos + 1;
            break;
        case 2:
            if (consumed && (unsigned char)s[0] == 0xED && end - s == 2
                && (unsigned char)s[1] >= 0xA0 && (unsigned char)s[1] <= 0xBF)
            {
                /* Truncated surrogate code in range D800-DFFF */
                goto End;
            }
            /* fall through */
        case 3:
        case 4:
            errmsg = "invalid continuation byte";
            startinpos = s - starts;
            endinpos = startinpos + ch - 1;
            break;
        default:
            if (_PyUnicodeWriter_WriteCharInline(&writer, ch) < 0)
                goto onError;
            continue;
        }

        // case 1、3、4的逻辑,会获取不同类型的error_handler,这里先忽略
    }

End:
    if (consumed)
        *consumed = s - starts;

    Py_XDECREF(error_handler_obj);
    Py_XDECREF(exc);
    return _PyUnicodeWriter_Finish(&writer);

onError:
    Py_XDECREF(error_handler_obj);
    Py_XDECREF(exc);
    _PyUnicodeWriter_Dealloc(&writer);
    return NULL;
}

unicode_decode_utf8做了以下几件事情:

  • size为1并且第一个字符值小于128时,通过get_latin1_char方法获取unicode实例
  • 采用PyUnicode_New初始化unicode实例并预设最大字符值为127,然后先尝试用ascii_decode将原生字符串转换为一个asciiunicode实例
    • 如果成功就return
    • 如果没成功,s会停在第一个非ascii字符的前面
      • 在上面的例子里,s也就表示"哈咯123"
  • 以先前的unicode实例为buffer,初始化PyUnicodeWriter实例处理非ASCII字符串,循环处理剩余的字符,写入到buffer
    • PyUnicodeWriter实例默认的kindPyUnicode_1BYTE_KIND。一般第一个字符会走到asciilib_utf8_decode逻辑,这个逻辑如果发现字符越界,会返回字符实际的code point
  • 发现第一个字符越界不能用ASCII表示,调用_PyUnicodeWriter_WriteCharInline逻辑写入字符
    • 调用_PyUnicodeWriter_Prepare逻辑,其中会根据第一个字符的值大小决定writer写入的字符类型kind
      • 汉字"哈"对应code point值为27014,即\u54c8。因此writerkind调整到PyUnicode_2BYTE_KIND,以适配汉字"哈"的写入
    • 调用PyUnicode_WRITE,写入字符"哈"buffer
  • writer.kindPyUnicode_2BYTE_KIND,下一个循环之后,调用ucs2lib_utf8_decode方法
    • 由于汉字基本是PyUnicode_2BYTE_KIND,通过ucs2lib_utf8_decode方法,就能把后面所有的字符都进行处理
  • 调用_PyUnicodeWriter_Finish,生成最终的unicode实例
    • 调用resize_compact,重新调整unicode实例数据

通过以上的操作,一个unicode字符串实例就生成了。

转载自:https://juejin.cn/post/7240419809179271226
评论
请登录