Python) pydantic 알아보기

2022. 3. 14. 23:09분석 Python/구현 및 자료

728x90

개인적으로는 dataclass가 namedtuple를 쓰다가 불편해서 사용하고 있었는데, 이 dataclass에서는 데이터 검증 부분이 없다고 한다.
그래서 우연히 pydantic을 알게 돼서 pydantic에 대해 정리해보고자 한다.

Python Type Hint를 사용한 데이터 유효성 검사 및 설정 관리에 대해서 설명하고자 합니다.
여기서는 pydantic이라는 패키지를 사용해봅니다.

pydantic은 validation library가 아닌 parsing 라이브러리리입니다.

유효성 검사는 제공된 유형 및 제약 조건을 준수하는 모델을 구축하는 목적을 위한 수단입니다.
즉, pydantic은 입력 데이터가 아닌 출력 모델의 유형과 제약 조건을 보장합니다.


우선 데이터 검증이 뭔지부터 살펴봅니다.

What is data validation?

데이터 유효성 검사는 데이터가 각 속성에 대해 정의한 일련의 규칙, 스키마 또는 제약 조건을 준수하도록 하는 프로세스입니다.
데이터 유효성 검사를 통과하면 코드가 예상했던 정확한 방식으로 데이터를 수집하고 반환합니다.

데이터 유효성 검사는 잘못된 사용자 입력과 같은 문제로 인해 발생하는 예기치 않은 오류를 방지합니다.
그런 의미에서 sanitization process라고도 합니다.

Installation

pip install pydantic

Optional dependencies

pydantic은 필요에 따라 다음과 같은 선택적 종속성과 함께 제공됩니다.

  • email-validator — Support for email validation.
  • typing-extensions — Support use of Literal prior to Python 3.8.
  • python-dotenv — Support for dotenv file with settings.
# install email-validator
pip install email-validator
# install typing-extensions
pip install typing_extensions
# install python-dotenv
pip install python-dotenv

# or 

# install email-validator
pip install pydantic[email]
# install typing-extensions
pip install pydantic[typing_extensions]
# install python-dotenv
pip install pydantic[dotenv]
# install all dependencies
pip install pydantic[email,typing_extensions,dotenv]


Implementation

 

Import

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel

pydantic에서 객체를 정의하는 것은 BaseModel에서 상속되는 새 클래스를 만드는 것만큼 간단합니다.
클래스에서 새 객체를 생성할 때 pydantic은 결과 모델 인스턴스의 필드가 모델에 정의된 필드 유형을 준수하도록 보장합니다.

User Class

class User(BaseModel):
    id: int
    username : str
    password : str
    confirm_password : str
    alias = 'anonymous'
    timestamp: Optional[datetime] = None
    friends: List[int] = []

pydantic은 내장된 type hint 구문을 사용하여 각 변수의 데이터 유형을 결정합니다.
뒤에서 어떤 일이 일어나는지 하나씩 살펴보겠습니다.

  • id - 이산형 변수는 ID를 표현합니다. 기본값은 제공하지 않기 때문에, 이 필드는 반드시 필요하고, 객체 생성할 때 정의가 되어야 합니다. 문자열, 바이트 또는 부동 소수점은 가능한 경우 정수로 강제 변환됩니다. 그렇지 않으면 예외가 발생합니다.
  • username - 문자형 변수는 username을 표현합니다. 그리고 이 필드는 반드시 필요합니다.
  • password - 문자형 변수는 password을 표현합하니다. 그리고 이 필드는 반드시 필요합니다.
  • confirm_password - 문자형 변수는 confirmation password를 표현합니다. 반드시 필요하고, 후에 데이터 검증을 위해서 사용됩니다.
  • alias - 문자형 변수는 alias를 표현합니다. 이것은 반드시 필요하지 않고, 설정하지 않으면 anonymous로 설정됩니다.
  • timestamp - date/time field를 의미하고, 반드시 필요하지는 않고, 기본 값은 None입니다.
  • friends - 이산형 변수들의 리스트를 의미합니다. 기본값은 []입니다.

Object instantiation

data = {'id': '1234', 'username': 'wai foong', 'password': 'Password123', 'confirm_password': 'Password123', 'timestamp': '2020-08-03 10:30', 'friends': [1, '2', b'3']}
user = User(**data)


user를 프린트하게 되면 id는 문자형을 넣었지만, 자동적으로 정수형으로 바뀌게 됩니다.
friends 역시 마찬가지로 비트는 자동적으로 정수형으로 바뀌게 됩니다.


Methods and attributes under BaseModel
BaseModel을 상속한 Class들은 다음 method들과 attribute들이 있습니다.

 

  • dict() — returns a dictionary of the model’s fields and values
  • json() — returns a JSON string representation dictionary
  • copy() — returns a deep copy of the model
  • parse_obj() — a utility for loading any object into a model with error handling if the object is not a dictionary
  • parse_raw() — a utility for loading strings of numerous formats
  • parse_field() — similar to parse_raw() but meant for files
  • from_orm() — loads data into a model from an arbitrary class
  • schema() — returns a dictionary representing the model as JSON schema
  • schema_json() — returns a JSON string representation of schema()
  • construct() — a class method for creating models without running validation
  • __fields_set__ — Set of names of fields which were set when the model instance was initialized
  • __fields__ — a dictionary of the model’s fields
  • __config__ — the configuration class for the model

 

만약 id를 문자형으로 바꾸게 되면 어떻게 되나 봅시다.
알오 같은 에러가 발생하게 됩니다.

data = {'id': 'a random string', 'username': 'wai foong', 'password': 'Password123', 'confirm_password': 'Password123', 'timestamp': '2020-08-03 10:30', 'friends': [1, '2', b'3']}
user = User(**data)

# value is not a valid integer (type=type_error.integer)

Validation Error

에러를 더 상세하기 하기 위해서 try-catch block안에 다가 하는 것이 좋습니다.
그러면 아래와 같이 print 된 결과를 얻을 수 있습니다.

data = {'id': 'a random string', 'username': 'wai foong', 'password': 'Password123', 'confirm_password': 'Password123', 'timestamp': '2020-08-03 10:30', 'friends': [1, '2', b'3']}
try:
    user = User(**data)
except ValidationError as e:
    print(e.json())

 

[
  {
    "loc": [
      "id"
    ],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }
]

Types

Field types

pydantic은 python 표준 라이브러리에 사용하는 공통 타입들을 대부분 지원합니다.

  • bool
  • int
  • float
  • str
  • bytes
  • list
  • tuple
  • dict
  • set
  • frozenset
  • datetime.date
  • datetime.time
  • datetime.datetime
  • datetime.timedelta
  • typing.Any
  • typing.TypeVar
  • typing.Union
  • typing.Optional
  • typing.List
  • typing.Tuple
  • typing.Dict
  • typing.Set
  • typing.FrozenSet
  • typing.Sequence
  • typing.Iterable
  • typing.Type
  • typing.Callable
  • typing.Pattern
  • ipaddress.IPv4Address
  • ipaddress.IPv4Interface
  • ipaddress.IPv4Network
  • ipaddress.IPv6Address
  • ipaddress.IPv6Interface
  • ipaddress.IPv6Network
  • enum.Enum
  • enum.IntEnum
  • decimal.Decimal
  • pathlib.Path
  • uuid.UUID
  • ByteSize

Constrained types

Constrained Type을 통해 자신의 제한을 적용할 수 있습니다. 

from pydantic import (
    BaseModel,
    NegativeInt,
    PositiveInt,
    conint,
    conlist,
    constr
)
class Model(BaseModel):
    # minimum length of 2 and maximum length of 10
    short_str: constr(min_length=2, max_length=10)
    # regex
    regex_str: constr(regex=r'^apple (pie|tart|sandwich)$')
    # remove whitespace from string
    strip_str: constr(strip_whitespace=True)

    # value must be greater than 1000 and less than 1024
    big_int: conint(gt=1000, lt=1024)
    
    # value is multiple of 5
    mod_int: conint(multiple_of=5)
    
    # must be a positive integer
    pos_int: PositiveInt
    
    # must be a negative integer
    neg_int: NegativeInt

    # list of integers that contains 1 to 4 items
    short_list: conlist(int, min_items=1, max_items=4)

Strict types

검증된 값이 해당 유형이거나 해당 유형의 하위 유형인 경우에만 유효성 검사를 통과하는 엄격한 제한을 찾고 있다면 다음과 같은 엄격한 유형을 사용할 수 있습니다.

  • StrictStr
  • StrictInt
  • StrictFloat
  • StrictBool
from pydantic import BaseModel, StrictBool,
class StrictBoolModel(BaseModel):
    strict_bool: StrictBool

문자로 나온 'False'는 True 또는 False만 입력으로만 가능하기 때문에 ValidationError를 발생시킵니다.

Validator

또한 상속된 클래스 내부의 유효성 검사기 데코레이터를 사용하여 고유한 사용자 지정 유효성 검사기를 만들 수 있습니다.

id가 4자리인지 확인하고 Confirm_password가 비밀번호 필드와 일치하는지 확인하는 다음 예를 살펴보겠습니다.

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ValidationError, validator
class User(BaseModel):
    id: int
    username : str
    password : str
    confirm_password : str
    alias = 'anonymous'
    timestamp: Optional[datetime] = None
    friends: List[int] = []
    @validator('id')
    def id_must_be_4_digits(cls, v):
        if len(str(v)) != 4:
            raise ValueError('must be 4 digits')
        return v
    @validator('confirm_password')
    def passwords_match(cls, v, values, **kwargs):
        if 'password' in values and v != values['password']:
            raise ValueError('passwords do not match')
        return v

아래와 같이 validator를 사용하여, id와 password를 검증해서 사용할 수 있습니다.

 

pydantic를 사용해야 하는 이유

 

1 — A simple syntax to define your data models

BaseModel 클래스에서 상속된 클래스 내에서 데이터를 정의할 수 있습니다.

Pydantic 모델은 데이터를 수집하고 구문 분석하고 데이터에 정의된 필드의 제약 조건을 준수하는지 확인하는 구조입니다.

from pydantic import BaseModel

class Person(BaseModel):
    first_name: str
    last_name: str

 

  • 데이터 클래스와 마찬가지로 유형 주석을 사용합니다.
  • 데이터 클래스와 달리 @dataclass 데코레이터를 사용하지 않습니다. 대신 BaseModel 클래스에서 상속합니다.

typing을 호출함으로써, 복잡한 필드를 추가할 수 있습니다.

from pydantic import BaseModel
from typing import Optional, List

class Person(BaseModel):
    first_name: str
    last_name: str
    interest: Optional[List[str]]

data = {"first_name": "Ahmed", "last_name": "Besbes"}
person = Person(**data)
print(person)

그리고 그 자체가 BaseModel 클래스인 유형을 생성할 수도 있습니다.

from pydantic import BaseModel
from typing import Optional, List

class Address(BaseModel):
    street: str
    number: int
    zipcode: str

class Person(BaseModel):
    first_name: str
    last_name: str
    interest: Optional[List[str]]

address_data = {"street": "Main street", "number": 1, "zipcode": 12345}
address = Address(**address_data)
data = {"first_name": "Ahmed", "last_name": "Besbes", "address": address}

person = Person(**data)
print(person)

2 — User-friendly error messages

Pydantic 모델을 정의하고 정의된 스키마와 일치하지 않는 일부 데이터를 전달하면 어떻게 됩니까?

이 특정 경우에 Pydantic이 어떻게 동작하는지 이해하기 위해 다음의 간단한 모델을 살펴보겠습니다.

from pydantic import BaseModel
from typing import Optional

class Address(BaseModel):
    street: str 
    number: int
    zipcode: str

class Person(BaseModel):
    first_name: str
    last_name: str
    age: int
    address: Optional[Address]

pydantic을 사용하면 어떤 것이 문제가 있는지 잘 프린트해서 보여준다고 합니다.

data = {'first_name" : "Ahead" , "last_name" : "temp"}
person = Person(**data)

3— Field customization

Pydantic을 사용하면 각 필드를 Field 클래스 내부에 warpping하여 기본적으로 일부 유효성 검사를 추가할 수 있습니다.

  • first_name은 최소 2글자에서 최대 20글자로 제한할 수 있습니다.
  • age는 150 이하로 설정할 수 있습니다. 
from pydantic import BaseModel, Field
from typing import Optional

class Address(BaseModel):
    street: str 
    number: int
    zipcode: str

class Person(BaseModel):
    first_name: str = Field(min_length=2, max_length=20)
    last_name: str
    age: int = Field(le=150)
    address: Optional[Address]

4 —A lot of helper methods

위에서 적은 것처럼 많은 method를 제공하고 있어서, 다양한 방식으로 사용할 수 있습니다.

  • dict() — returns a dictionary of the model’s fields and values
  • json() — returns a JSON string representation dictionary
  • copy() — returns a deep copy of the model
  • parse_obj() — a utility for loading any object into a model with error handling if the object is not a dictionary
  • parse_raw() — a utility for loading strings of numerous formats
  • parse_field() — similar to parse_raw() but meant for files
  • from_orm() — loads data into a model from an arbitrary class
  • schema() — returns a dictionary representing the model as JSON schema
  • schema_json() — returns a JSON string representation of schema()
  • construct() — a class method for creating models without running validation
  • __fields_set__ — Set of names of fields which were set when the model instance was initialized
  • __fields__ — a dictionary of the model’s fields
  • __config__ — the configuration class for the model

 

5 — Custom validators

새롭게 유효기 검사를 추가해서 검증할 수 있습니다.

import re
from pydantic import BaseModel, validator
from typing import Optional

class Address(BaseModel):
    street: str 
    number: int
    zipcode: str

class Person(BaseModel):
    first_name: str
    last_name: str
    phone_number: str
    age: int
    address: Optional[Address]
        
    @validator("phone_number")
    def phone_number_must_have_10_digits(cls, v):
        match = re.match(r"0\d{9}", v)
        if (match is None) or (len(v) != 10):
            raise ValueError("Phone number must have 10 digits")
        return v

6 — Parsing environment variable values

Pydantic을 사용하면 .env 파일에서 환경 변수를 읽고 BaseSettings 클래스 내에서 직접 구문 분석할 수 있습니다.

대신에 패키지 하나를 추가적으로 설치해줘야 합니다.

pip install python-dotenv

이러한 것이 필요한 이유는 파이썬에서 민감한 데이터 처리를 하드 코딩으로 하는 것을 방지합니다.

  • API keys to access third-party services
  • Passwords and credentials
  • Email addresses or personal data (name, age, social security number, etc.)
  • Debug flags
  • Hosts, URL, URI

이러한 설정 중 일부는 본질적으로 비공개입니다. 다른 사람들은 사이버 공격이나 위협에 대한 통찰력을 제공할 수 있기 때문에 민감합니다.

보통은 환경 변수를 만들어서 관리합니다.

환경 변수는 프로그램에 하드 코딩하고 싶지 않은 데이터를 보유하는 변수입니다.

그것들은 추상화되어 코드에서 제거됩니다.

 

이런 식으로. env 파일을 만들었다고 합시다.

LOGIN=Ahmed
API_KEY=SeCR€t!
SEED=42

Pydatnic이 이러한 변수를 로드하도록 하려면 먼저 BaseSettings 클래스에서 상속되는 Settings 클래스를 정의해야 합니다.

이렇게 설정하면, Settings 클래스 내에서 .env 파일에 나열된 변수를 정의하는 동시에 유형과 유효성 검사기를 추가합니다.

from pydantic import BaseSettings

class Settings(BaseSettings):
    api_key: str
    login: str
    seed: int
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()
print(settings)

Benchmarks

pydantic에서는 자신들이 가장 빠르다고 주장하고 있습니다.

 

이번에는 pydantic에 대해서 알아봤습니다.

parsing 하는 데이터가 문제가 있을 경우에 type hint를 통해서 강요할 수 있는 라이브러리입니다.

Reference

https://medium.com/towards-data-science/8-reasons-to-start-using-pydantic-to-improve-data-parsing-and-validation-4f437eae7678 

https://pydantic-docs.helpmanual.io/

 

728x90