我刚刚阅读了PEP 393并了解到Python的
str
类型根据内容使用不同的内部表示。所以,我做了一些实验,结果让我有点惊讶:
>>> sys.getsizeof('')
41
>>> sys.getsizeof('H')
42
>>> sys.getsizeof('Hi')
43
>>> sys.getsizeof('Ö')
61
>>> sys.getsizeof('Öl')
59
我知道在前三种情况下,字符串不包含任何非 ASCII 字符,因此可以使用每个字符 1 个字节的编码。将
Ö
等非 ASCII 字符放入字符串中会强制解释器使用不同的编码。因此,我对 'Ö'
比 'H'
占用更多空间并不感到惊讶。
但是,为什么
'Öl'
比'Ö'
占用的空间更少?我假设无论用于 'Öl'
的内部表示都允许更短的 'Ö'
表示。
我使用的是Python 3.12,显然它在早期版本中无法重现。
此测试代码(结构仅根据 3.12.4 源正确,即使如此我也没有仔细检查它们)
import ctypes
import sys
class PyUnicodeObject(ctypes.Structure):
_fields_ = [
("ob_refcnt", ctypes.c_ssize_t),
("ob_type", ctypes.c_void_p),
("length", ctypes.c_ssize_t),
("hash", ctypes.c_ssize_t),
("state", ctypes.c_uint64),
]
class StateBitField(ctypes.LittleEndianStructure):
_fields_ = [
("interned", ctypes.c_uint, 2),
("kind", ctypes.c_uint, 3),
("compact", ctypes.c_uint, 1),
("ascii", ctypes.c_uint, 1),
("statically_allocated", ctypes.c_uint, 1),
("_padding", ctypes.c_uint, 24),
]
def __repr__(self):
return ", ".join(f"{k}: {getattr(self, k)}" for k, *_ in self._fields_)
def dump_s(s: str):
o = PyUnicodeObject.from_address(id(s))
print("===")
print(f"{s=!r}, {o.length=}, {sys.getsizeof(s)=}")
state_int = o.state
state = StateBitField.from_buffer(ctypes.c_uint64(state_int))
print(state)
dump_s(str(chr(214)))
dump_s(str(chr(214) + chr(108)))
打印出来
===
s='Ö', o.length=1, sys.getsizeof(s)=61
interned: 0, kind: 1, compact: 1, ascii: 0, statically_allocated: 1, _padding: 0
===
s='Öl', o.length=2, sys.getsizeof(s)=59
interned: 0, kind: 1, compact: 1, ascii: 0, statically_allocated: 0, _padding: 43023
– 确凿无疑的证据似乎是
statically_allocated
于 Ö
。
我认为这源于 pycore_runtime_init_generated