python cpython关系_第3篇:CPython内部探究:PyASCIIObject的初始化
在CPython3.3之后,字符串對象發(fā)生了根本性的變法,本篇我們來討論一下字符串對象,在Include/unicodeobject.h,在整個源代碼的官方文檔可以歸納出幾點。在CPython3.3+之后,Unicode字符串分為有4種
緊湊型ASCII(Compact ASCII)
緊湊型ASCII也稱為ASCII限定字符串(ASCII only String).其對應(yīng)PyASCIIObject結(jié)構(gòu)體,該對象使用一個空間連續(xù)的內(nèi)存塊(一個內(nèi)部的state結(jié)構(gòu)體和一個wchar_t類型的指針),緊湊型ASCII只能涵蓋拉丁編碼以內(nèi)的字符。ASCII字符限定意味著PyASCIIObject只能U+0000 ~ U+007F這段區(qū)間的字符碼。
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* 字符串中的碼位個數(shù) */
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; /*C底層的寬字符序列以NUL結(jié)束*/
} PyASCIIObject;
ASCII限定字符串可以由PyUnicode_New函數(shù)使用其結(jié)構(gòu)體創(chuàng)建并設(shè)定state.ascii為1,state.compact為1。
從上面的類定義可知
length用于保存字符串中字符編碼的數(shù)量
hash用于緩存C級別字符串的哈系值。由于字符串對象是不可變對象,這樣避免每次重新計算該字符串的hash字段的值
state保存了保存了其子類實例的狀態(tài)信息,
wstr是緩存C字符串的一個wchar指針,當(dāng)然它是以“\0”結(jié)束
緊湊型Unicode(Compact Unicode)
其對應(yīng)PyCompactUnicodeObject結(jié)構(gòu)體,緊湊型Unicode以PyASCIIObject為基類,非ASCII字符串可以通過PyUnicode_New函數(shù)為PyCompactUnicodeObject分配內(nèi)存并設(shè)置state.compact=1
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* utf8中的字節(jié)數(shù),不包括結(jié)尾的\0. */
char *utf8; /* UTF-8表示形式(\0終止) */
Py_ssize_t wstr_length; /* wstr中的碼位個數(shù) */
} PyCompactUnicodeObject;
傳統(tǒng)的字符串(Legacy String)
其對應(yīng)PyUnicodeObject結(jié)構(gòu)體,傳統(tǒng)的字符串對象會其中會包含兩種特殊狀態(tài)not ready和ready。
傳統(tǒng)的字符串可以通過PyUnicode_FromUnicode為分配PyUnicodeObject結(jié)構(gòu)體分配內(nèi)存并封裝C級別的unicode字符串。 實際的字符串?dāng)?shù)據(jù)最初位于wstr塊中,并使用_PyUnicode_Ready函數(shù)復(fù)制到data的塊中。
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* 最小形式的Unicode緩沖區(qū) */
} PyUnicodeObject;
Unicode對象的原始基類除了PyObject外,是以PyASCIIObject繼承而來的,PyCompactUnicodeObject類繼承PyASCIIObject,PyUnicodeObject繼承自PyCompactUnicodeObject,那么整個CPython3.3+的字符串體系可以用如下圖表示
Unicode字符串的字節(jié)寬度
在了解字符串如何創(chuàng)建有一個非常關(guān)鍵概念,我們查看Include/cpython/unicodeobject.h源文件時,CPython內(nèi)部定義了一個叫PyUnicode_Kind的枚舉類型,PyUnicode_New函數(shù)在實例化一個字符串對象時,會使用PyUnicode_Kind的枚舉值設(shè)定字符串對象內(nèi)部類state.kind的值,該字段將告知CPython的其他內(nèi)部代碼如何解讀C底層的char指針指向的字符串?dāng)?shù)據(jù)。
enum PyUnicode_Kind {
/* String contains only wstr byte characters. This is only possible
when the string was created with a legacy API and _PyUnicode_Ready()
has not been called yet. */
PyUnicode_WCHAR_KIND = 0,
/* Return values of the PyUnicode_KIND() macro: */
PyUnicode_1BYTE_KIND = 1,
PyUnicode_2BYTE_KIND = 2,
PyUnicode_4BYTE_KIND = 4
};
字符串對象的內(nèi)存分配
前文說到PyASCIIObject對象和PyCompactUnicodeObject對象都可以通過PyUnicode_New函數(shù)來創(chuàng)建,那么該函數(shù)如何區(qū)分它創(chuàng)建的目標(biāo)是PyASCIIObject,還是PyCompactUnicodeObject呢?盡管兩者是"父子"的繼承關(guān)系,畢竟它們是不同的數(shù)據(jù)類型,仔細(xì)看一下實現(xiàn)代碼,大體上PyUnicode_New函數(shù)是根據(jù)maxchar來區(qū)分創(chuàng)建什么字符串對象的。
maxchar小于128,并且字符位寬為1個字節(jié),即標(biāo)準(zhǔn)的ASCII可識別的有效字符僅有128個,于是創(chuàng)建PyASCIIObject對象
maxchar小于256,并且字符位寬為1個字節(jié),PyUnicode_New就創(chuàng)建PyCompactUnicodeObject對象。對于256個字符碼位組成的字符集,稱為擴展的ASCII字符集(Extended ASCII Charset)
字節(jié)通常用于保存文本文檔中的各個字符。 在ASCII字符集中,每個0到127之間的二進(jìn)制值都被賦予一個特定字符。 大多數(shù)計算機擴展了ASCII字符集,以使用一個字節(jié)中可用的256個字符的整個范圍。 前128個字符處理特殊內(nèi)容,例如常見外語中的重音字符。
maxchar小于65536,并且字符位寬為2個字節(jié),PyUnicode_New就創(chuàng)建PyCompactUnicodeObject對象,這種情況PyCompactUnicodeObject對象實際保存的是utf-16編碼的字符串。
最后一種情況就是處理碼位個數(shù)大于65536且小于MAX_UNICODE,通常此類的字符串的編碼是utf-32
PyObject *
PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar)
{
PyObject *obj;
PyCompactUnicodeObject *unicode;
void *data;
enum PyUnicode_Kind kind;
int is_sharing, is_ascii;
Py_ssize_t char_size;
Py_ssize_t struct_size;
/*返回空字符串的PyObject包裝類 */
if (size == 0 && unicode_empty != NULL) {
Py_INCREF(unicode_empty);
return unicode_empty;
}
//處理ASCII字符集
is_ascii = 0;
is_sharing = 0;
struct_size = sizeof(PyCompactUnicodeObject);
if (maxchar < 128) {
kind = PyUnicode_1BYTE_KIND;
char_size = 1;
is_ascii = 1;
struct_size = sizeof(PyASCIIObject);
}
//處理ASCII擴展的字符集
else if (maxchar < 256) {
kind = PyUnicode_1BYTE_KIND;
char_size = 1;
}
//處理utf-16編碼的字符集
else if (maxchar < 65536) {
kind = PyUnicode_2BYTE_KIND;
char_size = 2;
if (sizeof(wchar_t) == 2)
is_sharing = 1;
}
//處理utf-32編碼的字符串
else {
if (maxchar > MAX_UNICODE) {
PyErr_SetString(PyExc_SystemError,
"invalid maximum character passed to PyUnicode_New");
return NULL;
}
kind = PyUnicode_4BYTE_KIND;
char_size = 4;
if (sizeof(wchar_t) == 4)
is_sharing = 1;
}
/* Ensure we won't overflow the size. */
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyUnicode_New");
return NULL;
}
if (size > ((PY_SSIZE_T_MAX - struct_size) / char_size - 1))
return PyErr_NoMemory();
/*
來自_PyObject_New()的重復(fù)分配代碼,而不是對PyObject_New()的調(diào)用,
因此我們能夠為對象及其數(shù)據(jù)緩沖區(qū)分配空間。
*/
obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);
if (obj == NULL)
return PyErr_NoMemory();
//綁定PyUnicode_Type的類型信息
obj = PyObject_INIT(obj, &PyUnicode_Type);
if (obj == NULL)
return NULL;
unicode = (PyCompactUnicodeObject *)obj;
if (is_ascii)
//obj指針移動
data = ((PyASCIIObject*)obj) + 1;
else
data = unicode + 1;
//設(shè)定state內(nèi)部類的狀態(tài)信息
_PyUnicode_LENGTH(unicode) = size;
_PyUnicode_HASH(unicode) = -1;
_PyUnicode_STATE(unicode).interned = 0;
_PyUnicode_STATE(unicode).kind = kind;
_PyUnicode_STATE(unicode).compact = 1;
_PyUnicode_STATE(unicode).ready = 1;
_PyUnicode_STATE(unicode).ascii = is_ascii;
if (is_ascii) {
//NULL結(jié)束符
((char*)data)[size] = 0;
_PyUnicode_WSTR(unicode) = NULL;
}
else if (kind == PyUnicode_1BYTE_KIND) {
((char*)data)[size] = 0;
_PyUnicode_WSTR(unicode) = NULL;
_PyUnicode_WSTR_LENGTH(unicode) = 0;
unicode->utf8 = NULL;
unicode->utf8_length = 0;
}
else {
unicode->utf8 = NULL;
unicode->utf8_length = 0;
if (kind == PyUnicode_2BYTE_KIND)
((Py_UCS2*)data)[size] = 0;
else /* kind == PyUnicode_4BYTE_KIND */
((Py_UCS4*)data)[size] = 0;
if (is_sharing) {
_PyUnicode_WSTR_LENGTH(unicode) = size;
_PyUnicode_WSTR(unicode) = (wchar_t *)data;
}
else {
_PyUnicode_WSTR_LENGTH(unicode) = 0;
_PyUnicode_WSTR(unicode) = NULL;
}
}
#ifdef Py_DEBUG
unicode_fill_invalid((PyObject*)unicode, 0);
#endif
assert(_PyUnicode_CheckConsistency((PyObject*)unicode, 0));
return obj;
}
PyUnicode_New函數(shù)在計算要為字符串對象分配的內(nèi)存后,即執(zhí)行下面這條語句后
obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);
那么PyASCIIObject的內(nèi)存分配如下圖
跟著會調(diào)用PyObject_INIT(obj, &PyUnicode_Type)函數(shù)來將PyUnicode_Type實例綁定到字符串對象的頭部。
OK!我們之前談?wù)揚yType_Type實例和各內(nèi)置數(shù)據(jù)類型的關(guān)系后,你應(yīng)該清楚字符串對象的初始化匹配對應(yīng)的PyUnicode_Type實例,我們關(guān)注的是tp_new字段的函數(shù)指針unicode_new
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 */
.....
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*/
....
(reprfunc) unicode_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
....
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_UNICODE_SUBCLASS, /* tp_flags */
unicode_doc, /* tp_doc */
.....
PyUnicode_RichCompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
unicode_iter, /* tp_iter */
0, /* tp_iternext */
unicode_methods, /* tp_methods */
....
&PyBaseObject_Type, /* tp_base */
....
unicode_new, /* tp_new */
PyObject_Del, /* tp_free */
};
若我們?yōu)橐韵伦址?分配內(nèi)存,對于CPython來說,它們默認(rèn)執(zhí)行utf-8執(zhí)行解碼也即29個字節(jié)
"我是一個自由開發(fā)者!!"
當(dāng)整個PyUnicode_New函數(shù)返回時,它構(gòu)建的PyASCIIObject如下內(nèi)存圖所示
字符串對象的初始化
一個簡單的例子,有想過在一個Python腳本中,一個字符串字面量如何在CPython內(nèi)部完成字符串對象的實例化嗎?對于CPython3.9來說,在實例化一個腳本內(nèi)固有的字符串(即單引號或雙引號內(nèi)),其實質(zhì)上從C級別的字符指針(const char*)指向的字符串字面量拷貝到PyUnicode_New函數(shù)分配的堆內(nèi)存的過程。而字符串初始化的函數(shù)調(diào)用起點為PyUnicode_DecodeUTF8Stateful函數(shù)。
該流程省略了很多unicode字節(jié)碼解碼等特殊情況而得到一個簡化的流程圖。經(jīng)過測試,幾乎所有Python腳本內(nèi)部所有字符串初始化的常規(guī)函數(shù)調(diào)用流程。
有人可能會問,你這個圖依據(jù)是怎么來的?我們已經(jīng)知道PyUnicode_New函數(shù)是一個為字符串對象間接分配內(nèi)存的函數(shù)接口,我們只要通過IDE工具查找并篩選引用該函數(shù)的上一個函數(shù)的結(jié)果,從中找到可能的函數(shù)調(diào)用路徑,并在各個可能的函數(shù)中插入一些printf函數(shù),打印函數(shù)名稱和相關(guān)傳入的關(guān)鍵參數(shù),就能推斷出該字符串對象初始化的軌跡了。還有慎用Python的Debug模型,因為你從IDE工具看到內(nèi)存狀態(tài)可能和運行時有所差異的。這個我在其他篇章也提到過。例如,我們在一個測試的test.py文件中,測試下面的Python字符串的實例化過程
"我是一個自由開發(fā)者!!"
那么執(zhí)行python腳本將所有打印的運行時信息重定向到一個文本中
./python test.py >debug.txt
如下圖所示,我們發(fā)現(xiàn)只要python的運行時系統(tǒng)不論調(diào)用模塊間的內(nèi)置函數(shù),還是用戶的自定義函數(shù),只要涉及Python字符串對象都依次遵循上面PyASCIIObject/PyUnicodeObject初始化的函數(shù)調(diào)用過程
unicode_decode_utf8函數(shù)
回歸正題,我們先看一下一個關(guān)鍵的函數(shù)unicode_decode_utf8,該函數(shù)的完整代碼見Objects/unicodeobject.c的第4979行-5122行,由于篇幅所限我這里將該函數(shù)拆解三個部分來討論,先查看第4979行第5088行.該函數(shù)第一個參數(shù)是const char*類型字符指針s,這里重點討論該函數(shù)和它調(diào)用的ascii_decode函數(shù)的一些細(xì)節(jié)問題。
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字符 */
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;
//假定參數(shù)s是一堆由ASCII碼位組成的字符串
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;
}
....
}
unicode_decode_utf8函數(shù)假定傳入的C級別的字符串分三種情況實例化字符串對象
第1種情況:僅包含一個字符且位于標(biāo)準(zhǔn)的ASCII字符集區(qū)間內(nèi)
此時調(diào)用get_latin1_char函數(shù)并返回,那么get_latin1_char函數(shù)主要做的事情就是在整個Python解釋器運行期間的緩存所有使用過的單個ASCII字符對象到一個長度為256的unicode_latin1靜態(tài)數(shù)組中。否則會為該字符調(diào)用PyUnicode_New函數(shù)分配內(nèi)存并緩存到unicode_latin1數(shù)組后再返回。
static PyObject*
get_latin1_char(unsigned char ch)
{
PyObject *unicode;
#ifdef LATIN1_SINGLETONS
unicode = unicode_latin1[ch];
//如果該字符已緩存在unicode_latin1中,立即返回
if (unicode) {
Py_INCREF(unicode);
return unicode;
}
#endif
//否則會為該字符分配內(nèi)存
unicode = PyUnicode_New(1, ch);
if (!unicode) {
return NULL;
}
PyUnicode_1BYTE_DATA(unicode)[0] = ch;
assert(_PyUnicode_CheckConsistency(unicode, 1));
#ifdef LATIN1_SINGLETONS
Py_INCREF(unicode);
unicode_latin1[ch] = unicode;
#endif
return unicode;
}
第2種情況:假定字符串長度不超過127,即由ASCII區(qū)間內(nèi)的任意編碼組成的字符串
這一邏輯推定的事實是前127個字符編碼(即ASCII字符集)是unicode字符集的一個子集。不論傳入的C級別字符串屬于哪一種情況,都需經(jīng)過一個特殊的ascii_decode函數(shù),這個ascii_decode函數(shù)對于在如下情況通常給unicode_decode_utf8函數(shù)返回0的偏移量
純ASCII字符串或純中文字符的unicode字符串
任意ASCII字符和多國unicode字符編碼混合的字符串
PS:具體的源代碼請查看下面代碼,關(guān)于該函數(shù)CPython源代碼文檔,以及官方網(wǎng)站的API說明都沒有提及,因此,我對其算法甚少理解,有大伙提供詳細(xì)信息,煩請跟帖評論留言。
static Py_ssize_t
ascii_decode(const char *start, const char *end, Py_UCS1 *dest)
{
const char *p = start;
const char *aligned_end = (const char *) _Py_ALIGN_DOWN(end, SIZEOF_LONG);
#if !defined(__m68k__)
#if SIZEOF_LONG <= SIZEOF_VOID_P
//斷言dest是按8字節(jié)對齊
assert(_Py_IS_ALIGNED(dest, SIZEOF_LONG));
if (_Py_IS_ALIGNED(p, SIZEOF_LONG)) {
/* Fast path, see in STRINGLIB(utf8_decode) for
an explanation. */
/* Help allocation */
const char *_p = p;
Py_UCS1 * q = dest;
while (_p < aligned_end) {
unsigned long value = *(const unsigned long *) _p;
if (value & ASCII_CHAR_MASK)
break;
*((unsigned long *)q) = value;
_p += SIZEOF_LONG;
q += SIZEOF_LONG;
}
p = _p;
while (p < end) {
if ((unsigned char)*p & 0x80)
break;
*q++ = *p++;
}
return p - start;
}
#endif
#endif
while (p < end) {
/* Fast path, see in STRINGLIB(utf8_decode) in stringlib/codecs.h
for an explanation. */
if (_Py_IS_ALIGNED(p, SIZEOF_LONG)) {
/* Help allocation */
const char *_p = p;
while (_p < aligned_end) {
unsigned long value = *(const unsigned long *) _p;
if (value & ASCII_CHAR_MASK)
break;
_p += SIZEOF_LONG;
}
p = _p;
if (_p == end)
break;
}
if ((unsigned char)*p & 0x80)
break;
++p;
}
memcpy(dest, start, p - start);
return p - start;
}
我們上面示例字符串在初始化時過程前,我們在其C函數(shù)內(nèi)用pinrtf函數(shù)的關(guān)鍵信息的輸出,編譯后運行如下圖
我們將上面的信息繪制成一個內(nèi)存圖,自然就一目了然啦。由于ascii_decode在函數(shù)返回后,對于任意的ASCII字符串對象或純Unicode編碼的字符串對象,p-start的偏移量始終為0.
ss8..png
還有更多的細(xì)節(jié),我們說本實例的字符串的長度是29字節(jié),前27個字節(jié)是unicode編碼,而最后兩個字節(jié)是純粹ASCII字符。其實UTF-8的思想是使用不同長度的字節(jié)序列對各種Unicode字符進(jìn)行編碼, 標(biāo)準(zhǔn)的ASCII字符,即包括拉丁字母數(shù)字和標(biāo)點符號使用一個字節(jié)、ASCII擴展字符都以2字節(jié)的順序排列、 韓文,中文和日文表意文字使用3字節(jié)序列。
小結(jié)
我們本篇討論了字符串對象的內(nèi)存分配PyUnicode_New函數(shù),以及提出了CPython3.3+的字符串初始化的函數(shù)調(diào)用路徑,先討論了unicode_decode_utf8函數(shù)和ascii_decode函數(shù)的一些細(xì)節(jié)問題。下一篇會討論剩下的unicode_decode_utf8代碼細(xì)節(jié)。
更新中.....
總結(jié)
以上是生活随笔為你收集整理的python cpython关系_第3篇:CPython内部探究:PyASCIIObject的初始化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python面向对象作业_python面
- 下一篇: python meshgrid_torc