В питоне часто любят обсуждать "мутабельные" и "иммутабельные" объекты, но крайне редко объясняют, в чем же на самом деле разница. Сегодня мы посмотрим на такое со стороны C.
PyObject
Все мы знаем, что в питоне все объект или
PyObject *, который упрощенно выглядит так (в FT сборке он посложнее):
struct _object {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
}
То есть: у нас есть только счетчик ссылок на объект и указатель на его тип. Первое меняется очень часто, если объект не immortal. Второе тоже можно менять в некоторых случаях:
>>> class A:
... __slots__ = ()
>>> class B:
... __slots__ = ()
>>> a = A()
>>> type(a)
<class '__main__.A'>
>>> a.__class__ = B
>>> type(a)
<class '__main__.B'>
Получается, что большинство объектов мутабельные уже на данном уровне.
Но, в целом есть три типа объектов, разные по уровню мутабельности:
1. Такие как
None: ob_refcnt не меняется (immortal), тип менять нельзя, ведь Py_TPFLAGS_IMMUTABLETYPE установлен (static type), размер неизменный 0 для всех потенциальных значений2. Такие как
int: ob_refcnt может меняться для больших чисел (маленькие инты - immortal), тип менять нельзя, размер нельзя менять, но он будет разный для разных чисел:
>>> sys.getsizeof(1)
28
>>> sys.getsizeof(10000000000000)
32
3. Такие как
list: ob_refcnt всегда меняется, тип менять нельзя, размер меняетсяОтдельно нужно отметить, что пользовательские классы обычно еще более мутабельны, потому что и тип менять можно.
Но, вопрос в другом: а где вообще хранится размер объекта и его внутренности? Раз в
PyObject ничего такого нет.C-API
В C-API питона есть два полезных макроса: PyObject_HEAD для объектов фиксированного размера и PyObject_VAR_HEAD для объектов, которые могут менять размер.
struct PyVarObject {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
};
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
Хотим поменять размер объекта? Увеличиваем
ob_size, аллоцируем новую память (если нужно) под новые объекты внутри.Итоговые объекты используют примерно такую логику:
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
*/
Py_ssize_t allocated;
} PyListObject;
То есть: на самом деле все объекты
list (и любые другие) не просто PyObject, они:- Имеют свой собственный тип:
PyListObject- Имеют общую абстракцию для размерности:
PyVarObject- Имеют общую абстракцию для типа и счетчика ссылок:
PyObjectЯ сделал небольшой очень упрощенный пример. Там я показываю в том числе, как происходит каст одного типа поинтера в другой в C.
Итог
1.
None не имеет внутреннего состояния вообще (не использует ничего)2.
int может иметь разный размер, но не может изменяться, потому использует PyObject_HEAD (раньше был PyObject_VAR_HEAD, там сложная история):
typedef struct _PyLongValue {
uintptr_t lv_tag; /* Number of digits, sign and flags */
digit ob_digit[1];
} _PyLongValue;
struct _longobject {
PyObject_HEAD
_PyLongValue long_value;
};
3.
list может иметь разный размер и может изменяться, потому использует PyObject_VAR_HEAD, как я показывал вышеОбсуждение: как вы думаете, как работает
len() для list? | Поддержать | YouTube | GitHub | Чат |