Given the following scenario:
import attrs
@attrs.define(kw_only=True)
class A:
values: list[float] = attrs.field(converter=float)
A(values=["1.1", "2.2", "3.3"])
which results in
*** TypeError: float() argument must be a string or a real number, not 'list'
Obviously it's due to providing the whole list to float
, but is there a way to get attrs do the conversion on each element, without providing a custom converter function?
CodePudding user response:
As far as I know, attrs
doesn't have a built-in option to switch conversion or validation to "element-wise", the way Pydantic's validators have the each_item
parameter.
I know you specifically did not ask for a converter function, but I don't really see much of an issue in defining one that you can reuse as often as you need to. Here is one way to implement a converter for your specific case:
from attrs import define, field
from collections.abc import Iterable
from typing import Any
def float_list(iterable: Iterable[Any]) -> list[float]:
return [float(item) for item in iterable]
@define
class A:
values: list[float] = field(converter=float_list)
if __name__ == '__main__':
a = A(values=["1.1", "2.2", "3.3"])
print(a)
It is not much of a difference to your example using converter=float
.
The output is of course A(values=[1.1, 2.2, 3.3])
.
You could even have your own generic converter factory for arbitrary convertible item types:
from attrs import define, field
from collections.abc import Callable, Iterable
from typing import Any, TypeAlias, TypeVar
T = TypeVar("T")
ItemConv: TypeAlias = Callable[[Any], T]
ListConv: TypeAlias = Callable[[Iterable[Any]], list[T]]
def list_of(item_type: ItemConv[T]) -> ListConv[T]:
def converter(iterable: Iterable[Any]) -> list[T]:
return [item_type(item) for item in iterable]
return converter
@define
class B:
foo: list[float] = field(converter=list_of(float))
bar: list[int] = field(converter=list_of(int))
baz: list[bool] = field(converter=list_of(bool))
if __name__ == '__main__':
b = B(
foo=range(0, 10, 2),
bar=["1", "2", 3.],
baz=(-1, 0, 100),
)
print(b)
Output: B(foo=[0.0, 2.0, 4.0, 6.0, 8.0], bar=[1, 2, 3], baz=[True, False, True])
The only downside to that approach is that the mypy
plugin for attrs
(for some reason) can not handle this type of converter function and will complain, unless you add # type: ignore[misc]
to the field definition in question.
CodePudding user response:
You could use cattrs, which is a companion library for attrs for transforming data.
So after a pip install cattrs
:
from functools import partial
import attrs
from cattrs import structure
@attrs.define(kw_only=True)
class A:
values: list[float] = attrs.field(converter=partial(structure, cl=list[float]))
print(A(values=["1.1", "2.2", "3.3"]))