2025, Dec 25 03:01
Почему ctypes.POINTER нельзя вызывать в __init_subclass__ и как правильно
Почему в Python ctypes возникает TypeError: _type_ must have storage info: чем опасен вызов POINTER в __init_subclass__ и как решить это через метакласс.
Когда вы определяете структуры в Python с помощью ctypes, возникает соблазн автоматически добавлять тип указателя к каждому подклассу сразу после его объявления. Но если попытаться сделать это в __init_subclass__, легко наткнуться на печально известную ошибку TypeError: _type_ must have storage info. Ниже — минимальный воспроизводимый пример и надежный прием, который помогает избежать этой ловушки.
Постановка задачи
Цель — создавать тип указателя для каждого нового подкласса ctypes.Structure в момент его объявления. Ниже — минимальный фрагмент, который приводит к ошибке.
import ctypes
from ctypes.wintypes import *
class BaseStruct(ctypes.Structure):
def __init_subclass__(subcls):
alias_ptr = ctypes.POINTER(subcls)
class OFFSET_VAL_V2_Q7(BaseStruct):
_fields_ = [('original', DOUBLE),
('max_delta', DOUBLE),
('cur_delta', DOUBLE)]
Этот код падает с TypeError: _type_ must have storage info при вызове POINTER. В то же время функционально эквивалентный подход — отложить создание указателя до завершения определения класса — работает без проблем.
import ctypes
from ctypes.wintypes import *
class BaseStruct(ctypes.Structure):
pass
class OFFSET_VAL_V2_Q7(BaseStruct):
_fields_ = [('original', DOUBLE),
('max_delta', DOUBLE),
('cur_delta', DOUBLE)]
ptr_t = ctypes.POINTER(OFFSET_VAL_V2_Q7)
Что происходит на самом деле
Суть — в моменте вызова __init_subclass__. Когда он запускается для подкласса, объект класса уже создан, но тело класса еще не отработало полностью. Для ctypes.Structure это означает, что _fields_ к тому моменту еще не задан. С точки зрения ctypes, у подкласса нет сведений о размещении (storage info), поэтому его нельзя использовать в качестве цели для POINTER. Второй пример срабатывает, потому что POINTER вызывается лишь после полного определения структуры, когда _fields_ уже на месте.
Решение: отложить создание указателя до финализации структуры
Если задача — автоматически объявлять тип указателя для каждого подкласса структуры, вызов POINTER должен происходить после полного построения класса. Изящный способ — метакласс: его __init__ выполняется уже после отработки тела класса. Ниже показан рабочий шаблон.
import ctypes
from ctypes.wintypes import *
class PtrAutoMeta(type(ctypes.Structure)):
def __init__(cls, name, bases, ns):
super().__init__(name, bases, ns)
if hasattr(cls, "_fields_"):
cls.PTYPE = ctypes.POINTER(cls)
class BaseStruct(ctypes.Structure, metaclass=PtrAutoMeta):
pass
class OFFSET_VAL_V2_Q7(BaseStruct):
_fields_ = [('original', DOUBLE),
('max_delta', DOUBLE),
('cur_delta', DOUBLE)]
# Использование
ptr_t = OFFSET_VAL_V2_Q7.PTYPE
Здесь метакласс проверяет наличие _fields_ и только после этого добавляет PTYPE, гарантируя, что ctypes видит полностью сформированную структуру со сведениями о размещении.
Почему это важно
ctypes строго относится к моменту, когда тип становится пригодным для использования как структура с выделенной памятью. Любая попытка вывести зависимые типы, например указатели, до того как установлен _fields_, закончится неудачей. Понимание жизненного цикла создания класса — и особенно порядка выполнения __init_subclass__, тела класса и хуков метакласса — помогает избегать тонких ошибок инициализации в низкоуровневом коде для межъязыкового взаимодействия.
Итоги
Не создавайте ctypes.POINTER в __init_subclass__ для подклассов ctypes.Structure: в этот момент структура еще не финализирована. Если нужны автоматические псевдонимы указателей, используйте метакласс и создавайте их после появления _fields_. Либо действуйте проще и объявляйте типы указателей явно после определения каждой структуры. В любом случае откладывание создания указателя до момента, когда у структуры есть сведения о размещении, делает ваш слой взаимодействия предсказуемым и безошибочным.