콘텐츠로 이동

??? api "API 문서" pydantic.dataclasses.dataclass

Pydantic의 BaseModel 사용하고 싶지 않다면 대신 표준 데이터 클래스 (Python 3.7에 도입됨)에서 동일한 데이터 유효성 검사를 받을 수 있습니다.

from datetime import datetime

from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    signup_ts: datetime = None


user = User(id='42', signup_ts='2032-06-21T12:00')
print(user)
"""
User(id=42, name='John Doe', signup_ts=datetime.datetime(2032, 6, 21, 12, 0))
"""

!!! 참고 명심하세요 pydantic.dataclasses.dataclass pydantic.BaseModel 을 대체 하지 않습니다 . pydantic.dataclasses.dataclass Pydantic 검증을 추가하여 dataclasses.dataclass 와 유사한 기능을 제공합니다. pydantic.BaseModel 서브클래싱하는 것이 더 나은 선택인 경우가 있습니다.

For more information and discussion see
[pydantic/pydantic#710](https://github.com/pydantic/pydantic/issues/710).

Pydantic 데이터 클래스와 BaseModel 의 차이점은 다음과 같습니다.

모든 표준 Pydantic 필드 유형을 사용할 수 있습니다. 그러나 생성자에 전달된 인수는 유효성 검사를 수행하고 필요한 경우 강제 변환을 수행하기 위해 복사됩니다.

Pydantic 데이터 클래스에서 유효성 검사를 수행하거나 JSON 스키마를 생성하려면 이제 데이터 클래스를 TypeAdapter로 래핑하고 해당 메서드를 사용해야 합니다.

default_factory 가 필요한 필드는 pydantic.Field 또는 dataclasses.field 로 지정할 수 있습니다.

import dataclasses
from typing import List, Optional

from pydantic import Field, TypeAdapter
from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])
    age: Optional[int] = dataclasses.field(
        default=None,
        metadata=dict(title='The age of the user', description='do not lie!'),
    )
    height: Optional[int] = Field(None, title='The height in cm', ge=50, le=300)


user = User(id='42')
print(TypeAdapter(User).json_schema())
"""
{
    'properties': {
        'id': {'title': 'Id', 'type': 'integer'},
        'name': {'default': 'John Doe', 'title': 'Name', 'type': 'string'},
        'friends': {
            'items': {'type': 'integer'},
            'title': 'Friends',
            'type': 'array',
        },
        'age': {
            'anyOf': [{'type': 'integer'}, {'type': 'null'}],
            'default': None,
            'description': 'do not lie!',
            'title': 'The age of the user',
        },
        'height': {
            'anyOf': [
                {'maximum': 300, 'minimum': 50, 'type': 'integer'},
                {'type': 'null'},
            ],
            'default': None,
            'title': 'The height in cm',
        },
    },
    'required': ['id'],
    'title': 'User',
    'type': 'object',
}
"""

pydantic.dataclasses.dataclass 의 인수는 model_config와 동일한 의미를 갖는 하나의 추가 키워드 인수 config 제외하고 표준 데코레이터와 동일합니다.

!!! warning v1.2 이후에는 pydantic 데이터 클래스 유형을 확인하려면 Mypy 플러그인을 설치해야 합니다.

유효성 검사기와 데이터 클래스 결합에 대한 자세한 내용은 데이터 클래스 유효성 검사기를 참조하세요.

데이터 클래스 구성

BaseModel 과 마찬가지로 config 수정하려면 다음 두 가지 옵션이 있습니다.

  • 데이터 클래스 데코레이터에 구성을 사전으로 적용
  • ConfigDict 구성으로 사용

    from pydantic import ConfigDict from pydantic.dataclasses import dataclass

    Option 1 - use directly a dict

    Note: mypy will still raise typo error

    @dataclass(config=dict(validate_assignment=True)) # (1)! class MyDataclass1: a: int

    Option 2 - use ConfigDict

    (same as before at runtime since it's a TypedDict but with intellisense)

    @dataclass(config=ConfigDict(validate_assignment=True)) class MyDataclass2: a: int

  • [API 참조][pydantic.config.ConfigDict.validate_할당]에서 validate_assignment 에 대해 자세히 알아볼 수 있습니다.

!!! note Pydantic 데이터 클래스는 초기화 프로그램에 전달된 추가 필드를 ignore forbid allow 하는 extra 구성을 지원합니다. 그러나 stdlib 데이터 클래스의 일부 기본 동작이 우선할 수 있습니다. 예를 들어, extra='allow' 사용하는 Pydantic 데이터 클래스에 있는 추가 필드는 데이터 클래스가 print 될 때 생략됩니다.

중첩된 데이터 클래스

중첩된 데이터 클래스는 데이터 클래스와 일반 모델 모두에서 지원됩니다.

from pydantic import AnyUrl
from pydantic.dataclasses import dataclass


@dataclass
class NavbarButton:
    href: AnyUrl


@dataclass
class Navbar:
    button: NavbarButton


navbar = Navbar(button={'href': 'https://example.com'})
print(navbar)
#> Navbar(button=NavbarButton(href=Url('https://example.com/')))

필드로 사용되는 경우 데이터 클래스(Pydantic 또는 바닐라)는 dict를 유효성 검사 입력으로 사용해야 합니다.

일반 데이터 클래스

Pydantic은 유형 변수가 있는 데이터 클래스를 포함하여 일반 데이터 클래스를 지원합니다.

from typing import Generic, TypeVar

from pydantic import TypeAdapter
from pydantic.dataclasses import dataclass

T = TypeVar('T')


@dataclass
class GenericDataclass(Generic[T]):
    x: T


validator = TypeAdapter(GenericDataclass)

assert validator.validate_python({'x': None}).x is None
assert validator.validate_python({'x': 1}).x == 1
assert validator.validate_python({'x': 'a'}).x == 'a'

데이터 클래스를 BaseModel 의 필드로 사용하거나 FastAPI를 통해 사용하는 경우 TypeAdapter 필요하지 않습니다.

Stdlib 데이터 클래스 및 Pydantic 데이터 클래스

stdlib 데이터 클래스에서 상속

Stdlib 데이터 클래스(중첩 여부에 관계없이)도 상속될 수 있으며 Pydantic은 상속된 모든 필드를 자동으로 검증합니다.

import dataclasses

import pydantic


@dataclasses.dataclass
class Z:
    z: int


@dataclasses.dataclass
class Y(Z):
    y: int = 0


@pydantic.dataclasses.dataclass
class X(Y):
    x: int = 0


foo = X(x=b'1', y='2', z='3')
print(foo)
#> X(z=3, y=2, x=1)

try:
    X(z='pika')
except pydantic.ValidationError as e:
    print(e)
    """
    1 validation error for X
    z
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='pika', input_type=str]
    """

BaseModel 과 함께 stdlib 데이터 클래스 사용

BaseModel 과 혼합되면 stdlib 데이터 클래스(중첩 여부에 관계없이)가 자동으로 Pydantic 데이터 클래스로 변환 된다는 점을 명심하세요! 또한 생성된 Pydantic 데이터 클래스는 원본과 정확히 동일한 구성 ( order , frozen ,...)을 갖습니다.

import dataclasses
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ConfigDict, ValidationError


@dataclasses.dataclass(frozen=True)
class User:
    name: str


@dataclasses.dataclass
class File:
    filename: str
    last_modification_time: Optional[datetime] = None


class Foo(BaseModel):
    # Required so that pydantic revalidates the model attributes
    model_config = ConfigDict(revalidate_instances='always')

    file: File
    user: Optional[User] = None


file = File(
    filename=['not', 'a', 'string'],
    last_modification_time='2020-01-01T00:00',
)  # nothing is validated as expected
print(file)
"""
File(filename=['not', 'a', 'string'], last_modification_time='2020-01-01T00:00')
"""

try:
    Foo(file=file)
except ValidationError as e:
    print(e)
    """
    1 validation error for Foo
    file.filename
      Input should be a valid string [type=string_type, input_value=['not', 'a', 'string'], input_type=list]
    """

foo = Foo(file=File(filename='myfile'), user=User(name='pika'))
try:
    foo.user.name = 'bulbi'
except dataclasses.FrozenInstanceError as e:
    print(e)
    #> cannot assign to field 'name'

사용자 정의 유형 사용

stdlib 데이터 클래스는 검증을 추가하기 위해 자동으로 변환되므로 사용자 정의 유형을 사용하면 예상치 못한 동작이 발생할 수 있습니다. 이 경우 구성에 arbitrary_types_allowed 추가하면 됩니다!

import dataclasses

from pydantic import BaseModel, ConfigDict
from pydantic.errors import PydanticSchemaGenerationError


class ArbitraryType:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f'ArbitraryType(value={self.value!r})'


@dataclasses.dataclass
class DC:
    a: ArbitraryType
    b: str


# valid as it is a builtin dataclass without validation
my_dc = DC(a=ArbitraryType(value=3), b='qwe')

try:

    class Model(BaseModel):
        dc: DC
        other: str

    # invalid as it is now a pydantic dataclass
    Model(dc=my_dc, other='other')
except PydanticSchemaGenerationError as e:
    print(e.message)
    """
    Unable to generate pydantic-core schema for <class '__main__.ArbitraryType'>. Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.

    If you got this error by calling handler(<some type>) within `__get_pydantic_core_schema__` then you likely need to call `handler.generate_schema(<some type>)` since we do not call `__get_pydantic_core_schema__` on `<some type>` otherwise to avoid infinite recursion.
    """


class Model(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    dc: DC
    other: str


m = Model(dc=my_dc, other='other')
print(repr(m))
#> Model(dc=DC(a=ArbitraryType(value=3), b='qwe'), other='other')

데이터 클래스가 pydantic 데이터 클래스인지 확인

Pydantic 데이터 클래스는 여전히 데이터 클래스로 간주되므로 dataclasses.is_dataclass 사용하면 True 반환됩니다. 유형이 구체적으로 Pydantic 데이터 클래스인지 확인하려면 다음을 사용할 수 있습니다. pydantic.dataclasses.is_pydantic_dataclass .

import dataclasses

import pydantic


@dataclasses.dataclass
class StdLibDataclass:
    id: int


PydanticDataclass = pydantic.dataclasses.dataclass(StdLibDataclass)

print(dataclasses.is_dataclass(StdLibDataclass))
#> True
print(pydantic.dataclasses.is_pydantic_dataclass(StdLibDataclass))
#> False

print(dataclasses.is_dataclass(PydanticDataclass))
#> True
print(pydantic.dataclasses.is_pydantic_dataclass(PydanticDataclass))
#> True

초기화 후크

데이터 클래스를 초기화할 때 @model_validator 데코레이터 mode 매개변수를 사용하여 유효성 검사 전후 코드를 실행할 수 있습니다.

from typing import Any, Dict

from typing_extensions import Self

from pydantic import model_validator
from pydantic.dataclasses import dataclass


@dataclass
class Birth:
    year: int
    month: int
    day: int


@dataclass
class User:
    birth: Birth

    @model_validator(mode='before')
    @classmethod
    def pre_root(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        print(f'First: {values}')
        """
        First: ArgsKwargs((), {'birth': {'year': 1995, 'month': 3, 'day': 2}})
        """
        return values

    @model_validator(mode='after')
    def post_root(self) -> Self:
        print(f'Third: {self}')
        #> Third: User(birth=Birth(year=1995, month=3, day=2))
        return self

    def __post_init__(self):
        print(f'Second: {self.birth}')
        #> Second: Birth(year=1995, month=3, day=2)


user = User(**{'birth': {'year': 1995, 'month': 3, 'day': 2}})

Pydantic 데이터 클래스의 __post_init__ 은 유효성 검사기 중간 에 호출됩니다. 순서는 다음과 같습니다.

  • model_validator(mode='before')
  • field_validator(mode='before')
  • field_validator(mode='after')
  • 내부 유효성 검사기. 예를 들어 int , str , ...과 같은 유형에 대한 유효성 검사
  • __post_init__ .
  • model_validator(mode='after')

    from dataclasses import InitVar from pathlib import Path from typing import Optional

    from pydantic.dataclasses import dataclass

    @dataclass class PathData: path: Path base_path: InitVar[Optional[Path]]

    def __post_init__(self, base_path):
        print(f'Received path={self.path!r}, base_path={base_path!r}')
        #> Received path=PosixPath('world'), base_path=PosixPath('/hello')
        if base_path is not None:
            self.path = base_path / self.path
    

    path_data = PathData('world', base_path='/hello')

    Received path='world', base_path='/hello'

    assert path_data.path == Path('/hello/world')

stdlib 데이터 클래스와의 차이점

Python stdlib의 dataclasses.dataclass 는 유효성 검사 단계를 실행하지 않으므로 __post_init__ 메서드만 구현합니다.

JSON 덤핑

Pydantic 데이터 클래스에는 .model_dump_json() 함수가 없습니다. JSON으로 덤프하려면 다음과 같이 RootModel 을 사용해야 합니다.

import dataclasses
from typing import List

from pydantic import RootModel
from pydantic.dataclasses import dataclass


@dataclass
class User:
    id: int
    name: str = 'John Doe'
    friends: List[int] = dataclasses.field(default_factory=lambda: [0])


user = User(id='42')
print(RootModel[User](User(id='42')).model_dump_json(indent=4))
"""
{
    "id": 42,
    "name": "John Doe",
    "friends": [
        0
    ]
}
"""

本文总阅读量