This works but feels a little ugly?
Unfortunately when metaprogramming, the code will tend to look a little dodgy. Looking at your answer we can see you solved the 'issue' by creating another function.
I agree with creating another function, as you move the less pleasant code out. Doing so allows you to DRY your code if you need to implement __sub__. However, I would take the operator you want to apply to the objects in as an argument to allow reuse of the function across different dunder methods.
I would also validate all the operators are actually of type T at run time. You can use the __debug__ dunder to disable the check when running Python in optimized mode; python -o.
def reduce_fields(
operator: Callable[[Any], Any],
self: T,
*others: T,
) -> T:
cls = type(self)
if __debug__:
for other in others:
if not isinstance(other, cls):
raise TypeError(f"{other!r} is not of type {cls.__name__}")
fields: dict[str, Any] = {}
for field in self.__fields__:
value = getattr(self, field)
for other in others:
value = operator(value, getattr(other, field))
fields[field] = value
return cls(**fields)
class NDVec(BaseModel):
# basic
dim1: int
dim2: int
# ....
dimN: int
def __add__(self, other: Self) -> Self:
return reduce_fields(operators.add, self, other)
The function reduce_fields is rather specialized. You can abstract the function to interact with dictionaries rather than types. Doing so allows for a more interoperable experience, with less jank in the reduce function. However the code is obviously a little less maintainable.
class IFields(Protocol):
@property
def __fields__(self) -> Iterable[str]: ...
def to_dict(obj: IFields) -> dict[str, Any]:
return {
key: getattr(obj, key)
for key in self.__fields__
}
def reduce_dict(
operator: Callable[[Any], Any],
source: dict[str, Any],
*others: dict[str, Any],
) -> dict[str, Any]:
if __debug__:
keys = set(source)
for i, other in enemurate(others, 1):
if (missing := set(other) - keys):
raise TypeError(f"other ({i}) missing keys {missing}")
fields: dict[str, Any] = {}
for key, value in source.items():
for other in others:
value = operator(value, other[field])
fields[field] = value
return fields
class NDVec(BaseModel):
# basic
dim1: int
dim2: int
# ....
dimN: int
def __add__(self, other: Self) -> Self:
return type(self)(**reduce_dict(operators.add, to_dict(self), to_dict(other)))