Post

FastAPI 공식문서 따라하기[17] - Extra Models

https://fastapi.tiangolo.com/tutorial/extra-models/ 공식문서 따라하는 글

☑️ Extra Models

쉬어가는 챕터로, 이전 예제에서 추가적인 개발을 하는 문서 같다

  • input modelpassword를 포함하고 있다.
  • output modelpassword를 포함하면 안된다.
  • database modelhashed password를 포함할 것이다.

☑️ Multiple models

모델을 설계할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

여기서의 문제점은 password를 그대로 가져오는것과 해시함수로 표현하지 않은것들인데, 예제에서는 복잡하지 않기 위해서 일단 그냥 넘기기로 했다.

하나 신기한 문법이 있다.

☑️ About **user_in.dict()

Pydantics’s .dict()

user_inUserIn라는 클래스의 Pydantic 모델이다.

이 모델은 .dict()이라는 함수를 가지고 있는데, 모델을 dict자료형으로 바꾸는 기능을 한다.

그렇기에

1
user_in = UserIn(username="kms", password="secret", email="minseokkang@gmail,com")

라고 생성된 Pydanticobject가 있다면 user_dict = user_in.dict()을 통해서 dict자료형으로 바꿀 수 있다.

1
2
3
4
5
6
7
#바뀐 자료형
{
    'username': 'kms',
    'password': 'secret',
    'email': 'minseokkang@gmail,com',
    'full_name': None,
}

이렇게 생성된 데이터를 함수에 인자를 통해 넘기고 싶으면 **user_dict을 통해 넘길 수 있다. 이를 Python에서는 “unwrap”이라고 한다. 그렇기에 위의 함수에서 처럼 UserInDB(**user_dict)로 넘길 수 있다.

이는 다음과 동등한 결과라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UserInDB(
    username="kms",
    password="secret",
    email = 'minseokkang@gmail,com',
    full_name=None,
)
또한

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

또한
UserInDB(**user_in.dict())
: user_in.dict()호출 -> `**`를 통한 "unwrap"

하지만 위의 예제에서는 두 번째 인자가 있다. 바로 hashed_password=hashed_password인데,

위의 코드는 다음과 동일하다고 한다.

1
2
3
4
5
6
7
UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

☑️ Reduce duplication

FastAPI가 코드의 중복을 줄여준다고 한다.

코드가 중복되면 예상치 못한 버그가 발생할 수 있고, 보안적인 이슈나 코드 동기화 문제도 있을 수 있다.

위의 코드에는 데이터가 많고 속성값과 타입도 각 클래스마다 중복되는 것들이 있다.

UserBase를 다른 클래스에서 상속받으면 해결이 될 것이다.

password와 관련된 속성을 제외하면 모두 중복된 코드이므로 다음과 같이 작성할 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

UserBase를 제외한 모든 클래스들은 UserBase를 상속받고 있음을 알자.

☑️ Union or anyOf

Response모델에 Union이란 키워드를 통해 두가지 타입을 지정할 수 있는데, 그 둘 중 하나의 클래스의 오브젝트타입으로 리턴한다는 것이다.

Union을 사용할 때 첫번째 타입은, 두 번째 타입보다 좀 더 중요하고 명시하고 싶은 타입을 선언한다. 밑의 예제는 PlaneItemCarItem보다 더 강조하고 싶다는 뜻이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type = "car"


class PlaneItem(BaseItem):
    type = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "planekms!!!!",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

http://localhost:8000/items/item2

로 접근하면

1
2
3
4
5
{
  "description": "Music is my aeroplane, it's my aeroplane",
  "type": "plane2",
  "size": 5
}

라는 결과를 얻을 수 있다.

참고로 Union[PlanItem, CarItem]repsonse_model = PlaneItem | CarItem으로 바꿔 쓸 수 있다. 공식 문서에서는 이는 에러를 발생시킨다고 되어있지만.. 실제로 해보면 된다. 다음 내용은 사실 쓰지 말라는 내용인데, 나는 잘 되어서 그 부분에 대한 내용은 추가하지 않겠다.

☑️ List of models

respone_model = List[Item]으로 선언이 가능하다는 얘기다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Father", "description": "There comes my hero"},
    {"name": "Job", "description": "It's my Life"},
]


@app.get("/items/", response_model=List[Item])
async def read_items():
    return items

보아하면 반환타입은 Item 형태를 가진 List다.

결과

1
2
3
4
5
6
7
8
9
10
[
  {
    "name": 123.4,
    "description": "There comes my hero"
  },
  {
    "name": 43.2,
    "description": "It's my Life"
  }
]

☑️ Response with arbitray dict

마찬가지로 dict타입으로 반환객체를 만들어줄 수 있다.

Pydantic모델과 달리 dict 모델은 클래스의 속성이름을 알 필요가 없다.

3.9 버전 이하에서는 from typing import Dict을 입력해야한다고 한다.

1
2
3
4
5
6
7
8
9
from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

어려운 것은 없으므로 설명은 생략

This post is licensed under CC BY 4.0 by the author.