Home > Software engineering >  attrs convert list[str] to list[float]
attrs convert list[str] to list[float]

Time:10-24

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"]))
  • Related