본문 바로가기

개발하자

python 데이터 클래스 __init_ 메서드에서 강제 형식 변환

반응형

python 데이터 클래스 __init_ 메서드에서 강제 형식 변환

다음과 같은 매우 간단한 데이터 클래스가 있습니다.

import dataclasses

@dataclasses.dataclass
class Test:
    value: int

클래스의 인스턴스를 만들지만 정수 대신 문자열을 사용합니다.

>>> test = Test('1')
>>> type(test.value)
<class 'str'>

내가 실제로 원하는 것은 클래스 정의에 정의된 데이터 유형으로 강제 변환하는 것입니다.

>>> test = Test('1')
>>> type(test.value)
<class 'int'>

방법을 수동으로 작성해야 하나요, 아니면 간단한 방법이 있나요?




데이터 클래스 속성의 유형 힌트는 유형이 시행되거나 검사된다는 점에서 절대 준수되지 않습니다. 와 같은 정적 유형 체커가 대부분 이 작업을 수행할 것으로 예상되며, 파이썬은 런타임에 이 작업을 수행하지 않습니다.

수동 유형 검사 코드를 추가하려면 다음 방법을 사용하십시오.

@dataclasses.dataclass
class Test:
    value: int

    def __post_init__(self):
        if not isinstance(self.value, int):
            raise ValueError('value not an int')
            # or self.value = int(self.value)

를 사용하여 필드와 유형을 지정하는 개체의 튜플을 가져와 각 필드에 대해 개별적으로 쓰지 않고 자동으로 이 작업을 수행할 수 있습니다.

def __post_init__(self):
    for field in dataclasses.fields(self):
        value = getattr(self, field.name)
        if not isinstance(value, field.type):
            raise ValueError(f'Expected {field.name} to be {field.type}, '
                             f'got {repr(value)}')
            # or setattr(self, field.name, field.type(value))



다음 방법을 사용하여 이를 달성할 수 있습니다.

import dataclasses

@dataclasses.dataclass
class Test:
    value : int

    def __post_init__(self):
        self.value = int(self.value)

이 메서드는 메서드를 따라 호출됩니다.

https://docs.python.org/3/library/dataclasses.html#post-init-processing




네, 쉬운 답은 그냥 당신 스스로 변환하는 거예요. 제가 원하는 건 제 물건을 원해서 하는 거예요.

형식 검증을 위해 Pydandic이 한다고 주장하지만, 나는 아직 시도하지 않았다.




를 사용하면 쉽게 달성할 수 있습니다.

데이터 클래스에서 데코레이터를 사용하십시오.

from dataclasses import dataclass
from pydantic import validate_arguments


@validate_arguments
@dataclass
class Test:
    value: int

그런 다음 데모를 시도해 보십시오. 'str type' 1이 에서 로 변환됩니다.

>>> test = Test('1')
>>> type(test.value)
<class 'int'>

정말 잘못된 유형을 통과하면 예외가 발생합니다.

>>> test = Test('apple')
Traceback (most recent call last):
...
pydantic.error_wrappers.ValidationError: 1 validation error for Test
value
  value is not a valid integer (type=type_error.integer)



파이썬을 사용하면 다른 답변에서 지적한 바와 같이 메소드를 사용할 수 있습니다.

@dataclasses.dataclass
class Test:
    value: int

    def __post_init__(self):
        self.value = int(self.value)
>>> test = Test("42")
>>> type(test.value)
<class 'int'>

또는 패키지를 사용하여 쉽게 설정할 수 있습니다.

@attr.define
class Test:
    value: int = attr.field(converter=int)
>>> test = Test("42")
>>> type(test.value)
<class 'int'>

데이터가 매핑에서 가져온 경우 의 유형 주석을 기반으로 변환하는 패키지를 사용할 수 있습니다.

@dataclasses.dataclass
class Test:
    value: int
>>> test = cattrs.structure({"value": "42"}, Test)
>>> type(test.value)
<class 'int'>

에서는 의 필드 유형에 따라 자동으로 변환을 수행합니다.

class Test(pydantic.BaseModel):
    value: int
>>> test = Test(value="42")
>>> type(test.value)
<class 'int'>



설명자 유형 필드를 사용할 수 있습니다.

class IntConversionDescriptor:

    def __set_name__(self, owner, name):
        self._name = "_" + name

    def __get__(self, instance, owner):
        return getattr(instance, self._name)

    def __set__(self, instance, value):
        setattr(instance, self._name, int(value))


@dataclass
class Test:
    value: IntConversionDescriptor = IntConversionDescriptor()
>>> test = Test(value=1)
>>> type(test.value)
<class 'int'>

>>> test = Test(value="12")
>>> type(test.value)
<class 'int'>

test.value = "145"
>>> type(test.value)
<class 'int'>

test.value = 45.12
>>> type(test.value)
<class 'int'>



에 선언된 일반 유형 변환을 사용할 수 있습니다.

import sys


class TypeConv:

    __slots__ = (
        '_name',
        '_default_factory',
    )

    def __init__(self, default_factory=None):
        self._default_factory = default_factory

    def __set_name__(self, owner, name):
        self._name = "_" + name
        if self._default_factory is None:
            # determine default factory from the type annotation
            tp = owner.__annotations__[name]
            if isinstance(tp, str):
                # evaluate the forward reference
                base_globals = getattr(sys.modules.get(owner.__module__, None), '__dict__', {})
                idx_pipe = tp.find('|')
                if idx_pipe != -1:
                    tp = tp[:idx_pipe].rstrip()
                tp = eval(tp, base_globals)
            # use `__args__` to handle `Union` types
            self._default_factory = getattr(tp, '__args__', [tp])[0]

    def __get__(self, instance, owner):
        return getattr(instance, self._name)

    def __set__(self, instance, value):
        setattr(instance, self._name, self._default_factory(value))

의 용도는 다음과 같습니다.

from __future__ import annotations
from dataclasses import dataclass
from descriptors import TypeConv


@dataclass
class Test:
    value: int | str = TypeConv()


test = Test(value=1)
print(test)

test = Test(value='12')
print(test)

# watch out: the following assignment raises a `ValueError`
try:
    test.value = '3.21'
except ValueError as e:
    print(e)

출력:

Test(value=1)
Test(value=12)
invalid literal for int() with base 10: '3.21'

이 기능은 다른 단순 유형에서는 작동하지만, 또는 등의 특정 유형에 대한 변환은 일반적으로 예상되는 대로 처리하지 않습니다.

이 작업에 타사 라이브러리를 사용하는 것이 괜찮으시다면, 다음을 호출할 때만 필요에 따라 유형 변환을 수행할 수 있는 (d)serialization 라이브러리를 생각해 냈습니다.

from __future__ import annotations
from dataclasses import dataclass

from dataclass_wizard import JSONWizard


@dataclass
class Test(JSONWizard):
    value: int
    is_active: bool


test = Test.from_dict({'value': '123', 'is_active': 'no'})
print(repr(test))

assert test.value == 123
assert not test.is_active

test = Test.from_dict({'is_active': 'tRuE', 'value': '3.21'})
print(repr(test))

assert test.value == 3
assert test.is_active



왜 안 써요?

from dataclasses import dataclass, fields

@dataclass()
class Test:
    value: int

    def __post_init__(self):
        for field in fields(self):
            setattr(self, field.name, field.type(getattr(self, field.name)))

필요한 결과를 산출합니다.

>>> test = Test('1')
>>> type(test.value)
<class 'int'>

반응형