Consider a simple data class:
from ctypes import c_int32, c_int16
from dataclasses import dataclass
@dataclass
class MyClass:
field1: c_int32
field2: c_int16
According to the docs, if we want to make this dataclass compatible with ctypes
, we have to define it like this:
import ctypes
from ctypes import Structure, c_int32, c_int16, sizeof
from dataclasses import dataclass, fields
@dataclass
class MyClass(ctypes.Structure):
_pack_ = 1
_fields_ = [("field1", c_int32),("field2", c_int16)]
print(ctypes.sizeof(MyClass))
But unfortunately, this definition deprives us of the convenient features of the dataclass, which are called “dunder” methods. For example, constructor(__init__()
) and string representation(__repr__()
) become unavailable:
inst = MyClass(c_int32(42), c_int16(43)) # will give error
Q: What is the most elegant and idiomatic way to make a dataclass compatible with ctypes without losing “dunder” methods?
If we ask me, this code seems to work at first glance:
@dataclass
class MyClass(ctypes.Structure):
field1: c_int32
field2: c_int16
_pack_ = 1
MyClass._fields_ = [(field.name, field.type) for field in fields(MyClass)] #_pack_ is skipped
Since I'm a beginner, I'm not sure if this code doesn't lead to some other, non-obvious problems.
Both ctypes.Structure and dataclass have some similar functionality - but neither was built with the explicit intent of being collaborative with the other - therefore we have to make this bridging code.
For one, the dataclass
decorator will always attempt to be less disruptive as it can to whatever functionalities the class already have. Since Structure
already provides an __init__
method, which works for it, we have to tell dataclass to leave it in place - this can be done just passing the init=False
argument to dataclass
: A Structure
class created this way will work, but won't have a __repr__
- dataclass needs the fields to be annotated in order to know its things.
The following decorator would work instead of @dataclass
:
def sdataclass(cls=None,/, **kwargs):
if cls is None:
return lambda cls: sdataclass(cls, **kwargs)
dataclassdeco = dataclass(init=False, **kwargs)
cls.__annotations__ = dict(cls._fields_)
return dataclassdeco(cls)
@sdataclass
class S(ctypes.Structure):
_fields_ = [("field1", ctypes.c_uint8),]
_pack_ = 1
The small amount of logic inside the decorator is just so that it can pass extra parameters to the original dataclass
call- the only important things there are setting the .__annotation__
fields and passing the init=False
parameter to the dataclass.
HOWEVER, dataclasses come at a later type than structures, when the annotation syntax is in place, and it is more convenient to declare a class fields than with the _fields_
parameter. A converse decorator can just do what you have done in the second part of your question - just passing the init=False
decorator.
(I am not sure the _pack_
can be set after the class is created - please test the functionality)
import ctypes
from dataclasses import fields, dataclass
def sdataclass(cls=None,/, **kwargs):
if cls is None:
return lambda cls: sdataclass(cls, **kwargs)
# Allow the pack information to be passed
# in the decorator:
pack = kwargs.pop("pack", True)
dataclassdeco = dataclass(init=False, **kwargs)
cls = dataclassdeco(cls)
cls._fields_ = [(field.name, field.type) for field in fields(cls)]
cls._pack_ = pack
return cls
...
@sdataclass
class S(ctypes.Structure):
field1: ctypes.c_uint8
field2: ctypes.c_uint16
of course, ctypes structures has some extra functionalities, like allowing the creation of bitfields - this won't suffice as it is - but it should be enough for composing complex structures as it is, without making use of forward declarations.