5. Classes#
/// A 2D vector with integer coordinates: comparisons, hashing, numbers, pickling. pub const Vec = struct { x: i64 = 0, // instance data: every field has a default y: i64 = 0, pub const basetype = true; // Python may subclass it /// How many coordinates a `Vec` has. pub const DIMS = 2; // UPPER_CASE: a class attribute /// Sets the coordinates. pub fn __init__(self: *Vec, a: struct { x: i64 = 0, y: i64 = 0 }) void { self.x = a.x; self.y = a.y; } /// `Vec(x, y)`. pub fn __repr__(self: *const Vec) Error!*Object { var buf: [64]u8 = undefined; // fits 2 x 20 digits const text = std.mem.print(&buf, "Vec({d}, {d})", .{ self.x, self.y }) catch unreachable; return py.new(text); } /// The squared length. pub fn norm2(self: *const Vec) i64 { return self.x * self.x + self.y * self.y; } /// A new vector of the class `cls` (a subclass too). pub fn from_pair(cls: *c.TypeObject, x: i64, y: i64) Error!*Object { const obj = c.PyObject_CallNoArgs(@ptrCast(cls)) orelse return error.PythonError; VecType.data(obj).* = .{ .x = x, .y = y }; return obj; } /// The dot product of two vectors. pub fn dot(a: *const Vec, b: *const Vec) i64 { return a.x * b.x + a.y * b.y; } }; pub const VecType = setu.Class(Vec, @embedFile("class.zig")); // declare once, use this name /// A `Vec` with a label: a Setu subclass, pickled with its base's fields. pub const Labeled = struct { label: ?*Object = null, // owned reference: traversed, cleared, dropped by Setu pub const base = VecType; /// Sets the label (the coordinates stay 0). pub fn __init__(self: *Labeled, label: ?*Object) void { if (label) |l| py.incref(l); py.decref(self.label); self.label = label; } /// The label. pub fn get_label(self: *const Labeled) *Object { const l = self.label orelse py.none(); py.incref(l); return l; } /// Sets the label; `None` is refused (a plain Zig error: `RuntimeError: NoLabel`). /// /// Type /// value: object pub fn set_label(self: *Labeled, value: *Object) error{NoLabel}!void { if (value == py.none()) return error.NoLabel; py.incref(value); py.decref(self.label); self.label = value; } }; pub const LabeledType = setu.Class(Labeled, @embedFile("class.zig"));
(Cut from test/class.zig, which gives Vec more hooks.) Stub: @disjoint_base\nclass Vec: with DIMS: ClassVar[int], def __init__(self, x: int = ..., y: int = ...) -> None:, def norm2(self) -> int:, @classmethod def from_pair(cls, x: int, y: int) -> Any:, @staticmethod def dot(a: Vec, b: Vec) -> int:; @final\nclass Labeled(Vec): with the property label. In Python:
>>> from pkg import Labeled, Vec >>> v = Vec(3, y=4) # __init__'s argument struct: positional or keyword >>> v, v.norm2(), Vec.DIMS (Vec(3, 4), 25, 2) >>> Vec(x='a') Traceback (most recent call last): ... TypeError: 'str' object cannot be interpreted as an integer >>> Vec.from_pair(1, 2), Vec.dot(Vec(1, 2), Vec(3, 4)) # a class method, a static method (Vec(1, 2), 11) >>> class Vec3(Vec): # basetype ... pass >>> type(Vec3.from_pair(1, 2)).__name__ 'Vec3' >>> l = Labeled('home') >>> l.label, isinstance(l, Vec) ('home', True) >>> l.label = 'work' # set_label >>> l.label 'work' >>> l.label = None Traceback (most recent call last): ... RuntimeError: NoLabel
Settings (pub const in the struct): name ("pkg.Name"; default <module>.<struct name>), base (another Class, created first), basetype, instantiable = false (only Zig creates instances, with new), getset / slots (extra c.GetSetDefs / c.Slots, with stub lines for them), weakref, dict (a __dict__), pickle (generated __getstate__ / __setstate__ over ints, floats, bools and ?*Objects of the type and its bases), plain_methods (.{"get_nowait"}: methods named like accessors), type_params ("T": Name[int] works, the stub says class Name[T]:), hidden (created, not added to the module, Any in the stub), and UPPER_CASE consts as class attributes.
Methods: a lowercase pub fn whose first parameter is named self (an instance method), cls: *c.TypeObject (a class method, @classmethod), anything else (a static method, @staticmethod). self is *T, *const T or *Object. get_x / set_x(self, value: U) are the property x. Parameters and results as in section 3; a py.Rest right after self is *args.
What the Class gives Zig: VecType.tp (the type object, after creation), VecType.data(obj) (the *Vec of an instance or subclass instance), VecType.object(d) (the object back from its data), VecType.check(obj), try VecType.new(.{ .x = 1 }) (a new instance, also of a type Python can not instantiate), setu.Instance(Vec).from(obj) (*Vec or TypeError).
Hooks, by name (the full list is at the top of src/class.zig; R is the hook's Zig result, Error!R is fine everywhere; a hook parameter is converted like a method argument, *Object keeps it raw, and a Path / buffer / Sequence parameter is a compile error since nothing would release it). Below: the hook (its slot): behaviour.
__new__(self, ...)(tp_new): after allocation and the field defaults; named parameters or onesetu.CallArgs__init__(self, ...)(tp_init): the same; gives the type's__text_signature____call__(self, ...)(tp_call): arguments parsed like a method's__traverse__(self, v: *setu.Visit) setu.Visit.Error!void(tp_traverse):try v.add(obj)beyond the?*Objectfields__clear__(self),__del__(self)(tp_clear,tp_dealloc): extra GC / dealloc work, after Setu's__finalize__(self)(tp_finalize): once, fields still set; may call Python; must not raise (PyErr_WriteUnraisable)__send__(self, arg: *Object) setu.Send(am_send,tp_iternext,tp_iter):.{ .next = v }yields,.{ .done = v }ends (new refs)__poll__(self, sent: ?*Object) setu.Poll(generated__await__/__send__):.pendingyieldsself;.{ .ready = v }ends; see below__await__(self) *Object(am_await): the iteratorawait objdrives__iter__(self) R,__next__(self) ?R(tp_iter,tp_iternext):nullends it; without__iter__the iterator is its own__len__(self) usize(mp_length)__getitem__(self, key: K) R(mp_subscript):keyconverted like an argument__setitem__(self, key: K, value: V),__delitem__(self, key: K)(mp_ass_subscript): a missing one raisesTypeError__contains__(self, item: I) bool(sq_contains): anitemof another type is not contained__bool__(self) bool,__repr__(self) R,__str__(self) R,__hash__(self) <int>(nb_bool,tp_repr,tp_str,tp_hash): -1 hashes as -2__eq__,__ne__,__lt__,__le__,__gt__,__ge__(self, other: O) bool(tp_richcompare): another type:NotImplemented; see below__add__,__sub__,__mul__,__truediv__,__floordiv__,__mod__,__divmod__(self, other: O) R(nb_*): see below__pow__,__lshift__,__rshift__,__and__,__or__,__xor__,__matmul__(self, other: O) R(nb_*): see below__r<op>__(self, other: O) R,__i<op>__(self, other: O) Rfor each of those (nb_*): reflected, in place; see below__neg__,__pos__,__abs__,__invert__(self) R(nb_*)__int__,__index__(self) <int>;__float__(self) <float>(nb_*): an int / a float result, checked at compile time__getattr__(self, name: []const u8) R(tp_getattro): after normal lookup raisedAttributeError__setattr__(self, name, value: V),__delattr__(self, name)(tp_setattro): every assignment / deletion; a missing one: the default__get__(self, instance: ?*Object, owner: ?*Object) R(tp_descr_get):instanceisnullwhen read on the class__set__(self, instance: *Object, value: V),__delete__(self, instance: *Object)(tp_descr_set)
Comparisons: != is not __eq__ without __ne__; __eq__ without __hash__ makes the type unhashable, as in Python. Number operators: __r<op>__ runs when the instance is on the right, __i<op>__ in place (a void result returns self); pow with a modulus raises TypeError. A __poll__'s sent is null on the first send, then what the loop sent in. The reflected and in-place names in full, for searching: __radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__, __rlshift__, __rrshift__, __rand__, __ror__, __rxor__, __rmatmul__; __iadd__, __isub__, __imul__, __itruediv__, __ifloordiv__, __imod__, __ipow__, __ilshift__, __irshift__, __iand__, __ior__, __ixor__, __imatmul__ (__idivmod__ is a compile error: Python has no in-place divmod).
A __send__ / __poll__ type may not also declare __iter__ / __next__ (generated); __poll__ and __send__ exclude each other. After .ready or an error, awaiting a __poll__ object again raises RuntimeError.