개요
외부에 request를 보내서 응답받는 데이터가 dictionary인 경우가 생긴다. 이를 다시 Project Source 내에서 적절한 Objects로 변환해서 사용해야 하는 상황일 때 써먹는 방법에 대한 글이다.
dataclass 변환에 사용할 dictionary
다음은 외부에 request를 보내서 받은 dictionary를 dataclass로 변환하기 위해 가정한 dictionary이다.
data = {
"account_balance": -9999,
"address": None,
"business_vat_id": None,
"created": 1694803249,
"currency": "eur",
"resource": {},
"meta": []
}
data에 들어있는 key에 대한 type에 주의해서 보자, int, str, None, dictionary, list가 포함되어 있다.
dataclass의 make_dataclass 사용하기
방법은 간단한다. python의 dataclass package에서 제공하는 make_dataclass를 사용하는 것이다.
from dataclasses import make_dataclass, fields
data = {
"account_balance": -9999,
"address": None,
"business_vat_id": None,
"created": 1694803249,
"currency": "eur",
"resource": {},
"meta": []
}
dataClass = make_dataclass("Data", data.keys())
print(dataClass(**data))
# OutPut
"""
Data(account_balance=-9999,
address=None,
business_vat_id=None,
created=1694803249,
currency='eur',
resource={},
meta=[])
"""
결과는 아주 깔끔하다. dictionary를 통해 dataclass를 만들 수 있었지만 "개요"에서 언급했듯이 이를 Project Source 내에서 사용하기 위해 Object로 변환하기 위한 과정에 문제가 존재한다.
class로 정의하기: dataclass의 fields를 이용하는 방법
여기서 언급하는 object로 만든다는 것은 어떤 py 파일에 class로서 이를 다시 정의한다는 것에 불과하다. 특히나 dataclass는 사용하기 편하기 때문에 attribute name과 그에 맞는 type만 있으면 빠르게 class로 정의할 수 있다. 이를 위해 make_class를 통해 나온 dataclass의 attribute의 type을 알아내보자.
from dataclasses import fields
for field in fields(dataKlass):
print(field)
dataclass의 fields를 이용했으며 결과는 다음과 같다.
Field(name='account_balance',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='address',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='business_vat_id',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='created',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='currency',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='resource',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
Field(name='meta',type='typing.Any',default=<dataclasses._MISSING_TYPE object at 0x102ea6990>,default_factory=<dataclasses._MISSING_TYPE object at 0x102ea6990>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
결과가 이상하다. 분명 정의된 type은 int, str, None, dictionary, list이다. 그런데 일괄되게 typing.Any가 나온다. 어떻게 된 걸까?
관련된 결과를 찾아보니 이는 PEP557에서 다음과 같이 언급되어 있었다.
make_dataclass(cls_name, fields, *, bases=(), namespace=None): Creates a new Data Class with name cls_name, fields as defined in fields, base classes as given in bases, and initialized with a namespace as given in namespace. fields is an iterable whose elements are either name, (name, type), or (name, type, Field). If just name is supplied, typing.Any is used for type.
"name"만 제공되는 경우 type에는 typing.Any가 사용된다고 한다. 이 사실을 알았으니 이제 다음과 같이 코드를 바꿔서 사용해 보자.
dataClass = make_dataclass("Data", [(k, type(v)) for k, v in data.items()])
dataKlass = dataClass(**data)
PEP557의 예제를 참고하여 만들었다. key와 value를 꺼내고 value는 type을 얻어 key, value를 tuple로 함께 만들어서 make_dataclass를 사용했다. 결과는 다음과 같다.
(Field(name='account_balance',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='address',type=<class 'NoneType'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='business_vat_id',type=<class 'NoneType'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='created',type=<class 'int'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='currency',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='resource',type=<class 'dict'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
Field(name='meta',type=<class 'list'>,default=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,default_factory=<dataclasses._MISSING_TYPE object at 0x102afa9d0>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD))
여기까지 하면 class로 선언하기 위한 준비는 마쳤다. 조금 숟가락을 더해서 다음과 같이 class를 만들어내자.
for f in fields(dataKlass):
name = f.name
_type = str(f.type).split("'")[1]
print(f"{name}:{_type}")
# OutPut
"""
account_balance:int
address:NoneType
business_vat_id:NoneType
created:int
currency:str
resource:dict
meta:list
"""
# dataclass로 만들어내기
@dataclass
class Data:
account_balance: int
address: None
business_vat_id: None
created: int
currency: str
resource: dict
meta: list
불편한 점은 NoneType을 dataclass로 정의할 때는 타입에 None을 사용해줘야 한다는 점이다.
class로 정의하기: built-in 함수 이용하기
dataclass의 module을 이용하는 방법이 다소 번거롭다면 다음과 같은 코드를 이용해서 class로 만들어낼 수도 있다.
for ns in dir(dataKlass):
if ns.startswith("__"):
continue
print(ns, str(type(getattr(dataKlass, ns))).split("'")[1])
결과는 다음과 같다.
account_balance int
address NoneType
business_vat_id NoneType
created int
currency str
meta list
resource dict
마치며
아이디어는 항상 문제에 직면했을 때 떠오른다. 이번 케이스도 그러했는데 모르는 사용사례를 하나 알게 되어 좋다.
'Language > Python' 카테고리의 다른 글
LangChain으로 크롤링을 해보자. (0) | 2023.10.17 |
---|---|
pyenv를 알아보자. (0) | 2023.09.18 |
[SQLAlchemy] SQL Compilation Caching (0) | 2023.08.13 |
[Python] Selenium Proxy를 이용한 Tor 사용하기 (0) | 2023.07.09 |
pytest-django에서 view에 request 던지기 (2) | 2023.06.18 |