Вы знаете, Python представляет каждый объект, используя низкоуровневую структуру C API
PyObject (или
PyVarObject для объектов переменного размера), так что, конкретно, вы можете привести любой указатель объекта Python к этому типу; это наследование создается вручную, каждый новый объект должен иметь ведущий макрос
PyObject_HEAD, который определяет
заголовок PyObject для объекта. Структура
PyObject объявлена в
файле Include / object.h как:
Первоначально Автор Кристиан С. Пероне
typedef struct _object { PyObject_HEAD } PyObject;
и макрос PyObject_HEAD определяется как:
#define PyObject_HEAD \ _PyObject_HEAD_EXTRA \ Py_ssize_t ob_refcnt; \ struct _typeobject *ob_type;
… С двумя полями (
забудьте _PyObject_HEAD_EXTRA , он используется только для функции отладки трассировки ) с
именами ob_refcnt и
ob_type , представляющими счетчик ссылок для объекта и тип объекта. Я знаю, что вы можете использовать
sys.getrefcount для подсчета ссылок на объект, но взлом памяти объекта с помощью ctypes намного более эффективен, поскольку вы можете получить содержимое любого поля объекта (в случаях, когда вы этого не делаете есть собственный API для этого), я покажу больше примеров позже, но давайте сосредоточимся на поле подсчета ссылок объекта.
Получение количества ссылок (ob_refcnt)
Итак, в Python у нас есть встроенная функция id () , эта функция возвращает идентификатор объекта, но, посмотрев на его определение по реализации CPython, вы заметите, что id () возвращает адрес памяти объекта см. источник в Python / bltinmodule.c :
static PyObject * builtin_id(PyObject *self, PyObject *v) { return PyLong_FromVoidPtr(v); }
… Функция PyLong_FromVoidPtr возвращает длинный объект Python из пустого указателя. Итак, в CPython это значение является адресом объекта в памяти, как показано ниже:
>>> value = 666 >>> hex(id(value)) '0x8998e50' # memory address of the 'value' object
Теперь, когда у нас есть адрес памяти объекта, мы можем использовать модуль Python ctypes для подсчета ссылок, используя атрибут ob_refcnt , вот код, необходимый для этого:
>>> value = 666 >>> value_address = id(value) >>> >>> ob_refcnt = ctypes.c_long.from_address(value_address) >>> ob_refcnt c_long(1)
Здесь я получаю целочисленное значение из атрибута ob_refcnt объекта PyObject в памяти. Давайте добавим новую ссылку для созданного нами объекта «value», а затем снова проверим счетчик ссылок:
>>> value_ref = value >>> id(value_ref) == id(value) True >>> ob_refcnt c_long(2)
Обратите внимание, что счетчик ссылок был увеличен на 1 из-за новой переменной ссылки под названием ‘value_ref’.
Состояние интернированных строк (ob_sstate)
Теперь подсчет ссылок был даже не смешным, для этого у нас уже был API sys.getrefcount , но как насчет интернированного состояния строк ? Чтобы избежать создания различных выделений для одной и той же строки (и для ускорения сравнений), Python использует словарь, который работает как «кеш» для строк, этот словарь определен в Objects / stringobject.c :
/* This dictionary holds all interned strings. Note that references to strings in this dictionary are *not* counted in the string's ob_refcnt. When the interned string reaches a refcnt of 0 the string deallocation function will delete the reference from this dictionary. Another way to look at this is that to say that the actual reference count of a string is: s->ob_refcnt + (s->ob_sstate?2:0) */ static PyObject *interned;
Я также скопировал здесь комментарий о словаре, потому что интересно отметить, что строки в словаре не учитываются в строке ob_refcnt .
Итак, интернированное состояние строкового объекта сохраняется в атрибуте ob_sstate строкового объекта, давайте посмотрим определение строкового объекта Python:
typedef struct { PyObject_VAR_HEAD long ob_shash; int ob_sstate; char ob_sval[1]; /* Invariants: * ob_sval contains space for 'ob_size+1' elements. * ob_sval[ob_size] == 0. * ob_shash is the hash of the string or -1 if not computed yet. * ob_sstate != 0 iff the string object is in stringobject.c's * 'interned' dictionary; in this case the two references * from 'interned' to this object are *not counted* in ob_refcnt. */ } PyStringObject;
Как вы можете заметить, строковые объекты наследуются от макроса PyObject_VAR_HEAD, который определяет другой атрибут заголовка, давайте посмотрим определение, чтобы получить полное представление о структуре:
#define PyObject_VAR_HEAD \ PyObject_HEAD \ Py_ssize_t ob_size; /* Number of items in variable part */
PyObject_VAR_HEAD макрос добавляет еще одно поле , называемое ob_size , который является количество элементов на переменной части объекта Python (то есть количество элементов на объект списка). Поэтому, прежде чем перейти к полю ob_sstate , нам нужно сместить наше смещение, чтобы пропустить поля ob_refcnt (long) , ob_type (void *) (из PyObject_HEAD ), поле ob_size (long) (из PyObject_VAR_HEAD ) и поле ob_shash (long ) из PyStringObject . Конкретно, нам нужно пропустить это смещение (3 поля с размером long и одно поле с размером void *байт)
>>> ob_sstate_offset = ctypes.sizeof(ctypes.c_long)*3 + ctypes.sizeof(ctypes.c_voidp) >>> ob_sstate_offset 16
Теперь давайте подготовим два случая, один из которых, как мы знаем, не интернирован, а другой, безусловно, интернирован, а затем заставим интернированное состояние другой не интернированной строки проверить результат атрибута ob_sstate :
>>> a = "lero" >>> b = "".join(["l", "e", "r", "o"]) >>> ctypes.c_long.from_address(id(a) + ob_sstate_offset) c_long(1) >>> ctypes.c_long.from_address(id(b) + ob_sstate_offset) c_long(0) >>> ctypes.c_long.from_address(id(intern(b)) + ob_sstate_offset) c_long(1)
Обратите внимание, что внутреннее состояние для объекта «a» равно 1, а для объекта «b» — 0. После принудительного определения внутреннего состояния переменной «b» мы можем видеть, что поле ob_sstate изменилось на 1.
Изменение внутренних состояний (режим зла)
Теперь предположим, что мы хотим изменить некоторое внутреннее состояние объекта Python через интерпретатор. Попробуем изменить значение объекта int. Объекты Int определены в файле Include / intobject.h :
typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
Как видите, внутреннее значение int хранится в поле ob_ival , для его изменения нам просто нужно пропустить ob_refcnt (long) и ob_type (void *) из PyObject_HEAD :
>>> value = 666 >>> ob_ival_offset = ctypes.sizeof(ctypes.c_long) + ctypes.sizeof(ctypes.c_voidp) >>> ob_ival = ctypes.c_int.from_address(id(value)+ob_ival_offset) >>> ob_ival c_long(666) >>> ob_ival.value = 8 >>> value 8
И это все, мы изменили значение int прямо в памяти.
Надеюсь, вам понравилось, вы можете поиграть с множеством других объектов Python, таких как списки и дикты, обратите внимание, что этот метод предназначен просто для того, чтобы показать, как объекты Python структурированы в памяти и как вы можете изменить их с помощью собственного API, но очевидно, вы не должны использовать это, чтобы изменить значение целых чисел.
Обновление 29.11.11 : вы не должны делать такие вещи в своем производственном коде или что-то в этом роде, в этом посте я делаю ленивые предположения о деталях арки, таких как размеры примитивов и т. Д. Будьте осторожны .