Настоящее содержимое памяти восьми объектов, снятое ctypes.string_at(id(obj), sizeof) и разложенное по полям.
Механизм, описанный словами, и механизм, который покрутил руками, оседают по-разному. Ниже — не иллюстрация, а дамп: каждый байт снят из живого процесса, каждое поле проверено на совпадение с известным значением (счётчик, длина, хеш, указатель на тип). Наведите курсор на поле в таблице — соответствующие байты подсветятся.
Всё, что здесь показано, снимается пятью строками. Вставляется в терминал или ноутбук как есть:
import ctypes, sys, struct
obj = 2**70
n = sys.getsizeof(obj)
raw = ctypes.string_at(id(obj), n)
print(n, raw.hex(' '))
print("refcnt:", struct.unpack_from("<q", raw, 0)[0])
print("type is int:", struct.unpack_from("<Q", raw, 8)[0] == id(int))
print("ob_size:", struct.unpack_from("<q", raw, 16)[0])
print("digits:", [struct.unpack_from("<I", raw, 24 + 4*i)[0] for i in range(3)])
Аккуратно: string_at читает сырую память по адресу. Ошибка в длине — сегфолт всего процесса, а не исключение. Это ровно та граница, за которой Python перестаёт быть безопасным языком.
int — 24 байта против 4 полезных, у строки «hello» — 48 против 5. Единая объектная модель оплачивается здесь, в байтах, на каждом объекте.ob_item — адрес чужого блока. Отсюда и промахи кеша при обходе, и numpy.__slots__ превращает объект в структуру C. Атрибут по фиксированному смещению вместо просмотра словарей: 56 байт на инстанс против 96, и 160 — если кто-то в коде тронул __dict__.wstr у строк, которое здесь занимает восемь байт, в 3.12 просто удалили.