본문으로 바로가기
728x90
반응형

개요

외부에 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

 

마치며

아이디어는 항상 문제에 직면했을 때 떠오른다. 이번 케이스도 그러했는데 모르는 사용사례를 하나 알게 되어 좋다.

 


 

728x90
반응형