dataclass

Published by onesixx on

https://youtu.be/CvQ7e6yUtnw

import random
import string

def generate_id() -> str:
    return "".join(random.choices(string.ascii_uppercase, k=12))

#generate_id()

class Person:
    def __init__(self, name:str, address:str):
        self.name = name
        self.address = address

def main() -> None:
    person = Person(name="john", address="yatop-ro 162")
    print(person)

if __name__ == "__main__":
    main()

활용이 어렵다. Person의 name이나 addressr

 $ conda run -n notidash --no-capture-output --live-stream python /Users/onesixx/my/git/noti-api/examtest/dataclass_ex.py
<__main__.Person object at 0x7f8861d3cf10>
class Person:
    def __init__(self, name:str, address:str):
        self.name = name
        self.address = address
    def __str__(self) -> str:
        return f"{self.name}, {self.address}"
 john, yatop-ro 162
import random
import string
from dataclasses import dataclass

def generate_id() -> str:
    return "".join(random.choices(string.ascii_uppercase, k=12))
@dataclass
class Person:
    name:str
    address:str

def main() -> None:
    person = Person(name="john", address="yatop-ro 162")
    print(person)

if __name__ == "__main__":
    main()
Person(name='john', address='yatop-ro 162')

dataclass라ㄴ

https://sjquant.tistory.com/30

파이썬 3.7부터 도입된 dataclasses에 대해 알아보자

from dataclasses import dataclass, field

...(생략)

@dataclass
class Client:
    interface: websockets.server.WebSocketServerProtocol = field(repr=False)
    sid: str = field(default_factory=partial(generate_code, 36))

    ...(생략)
  • 파이썬 3.7부터 표준 라이브러리로 등재
  • 이는 데이터 클래스를 보다 용이하게 선언해주는 데코레이터
  • Type Annotation을 지원.
  • 클래스에 @dataclass 데코레이터를 사용하면 __int____repr____eq__ 등의 메소드를 자동으로 정의해 준다.
from dataclasses import dataclass

# 이렇게 @dataclass 데코레이터를 클래스 위에 붙여준다.
@dataclass
class Item:
    id: int
    name: str


print(Item(1, "Apple"))
print(Item(2, "Banana"))

실행결과

Item(id=1, name='Apple')
Item(id=2, name='Banana')
  • 위의 코드 같은 경우 데코레이터를 사용하지 않는다면 __init__메소드에 id와 name을 변수로 받는 것이 없기 때문에 에러가 발생한다.
  • 만약 __init__메소드를 정의했다하더라도, __repr__메소드가 정의되어 있지 않기 때문에 아래와 같이 출력된다.
class Item:
    id: int
    name: str

    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name


print(Item(1, "Apple"))
print(Item(2, "Banana"))
class Item:
    id: int
    name: str

    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name


print(Item(1, "Apple"))
print(Item(2, "Banana"))

실행결과

>>
<__main__.Item object at 0x7fd501796898>
<__main__.Item object at 0x7fd501796898
  • 데이터를 정의하는 클래스라는 점에서 기존 파이썬의 collections.namedtuple 비슷하다. 하지만,@dataclass를 사용한 클래스는 말 그대로 클래스라는 점에서 메소드나 프로퍼티를 추가할 수 있다.
  • dataclasses.field를 사용하면 해당 프로퍼티에 대한 설정을 해줄 수 있다. (ex: default_factoryrepr여부)
from dataclasses import dataclass, field
from functools import partial
import uuid


@dataclass
class Factory:
    id = field(default_factory=partial(uuid.uuid4))
    items = field(default_factory=list)


f1 = Factory()
print(f1.items)
f1.items += ['apple', 'banana']
print(f1.items)
print(f1)

실행결과

[]
['apple', 'banana']
Factory(id=UUID('990091ef-6058-4754-b1f5-78e3546cc7bd'), items=['apple', 'banana'])
  • 파이썬의 Type Annotation을 활용하긴하지만, 해당 타입이 맞는지 검증(validate)하지는 않는다.
  • 만약 dataclasses를 사용하고 검증까지 원한다면 pydantic의 pydantic.dataclasses를 사용하면 된다. (pydantic 라이브러리는 추후에 제대로 공부해서 정리하겠다.) [참고]

ex

from dataclasses import dataclass, asdict
from os          import path, environ

base_dir =path.dirname(path.abspath(__file__))
# 데코레이더를 활용해 데이터클래스 만들기
@dataclass
class mainConfig():
    BASE_DIR:str = base_dir

# 상속
@dataclass
class localConfig(mainConfig):
    HOST:str = '127.0.0.1'
    PORT:int = 8085
    DEBUG:bool = True
    DEV_TOOLS_PROPS_CHECK:bool = True

print(mainConfig())
# mainConfig(BASE_DIR='/Users/onesixx/my/git/dash-adminlte')
localConfig()
#localConfig(BASE_DIR='/Users/onesixx/my/git/dash-adminlte', 
# HOST='127.0.0.1', PORT=8085, DEBUG=True, DEV_TOOLS_PROPS_CHECK=True)
localConfig().PORT
#808
# 객체는 unpacking이 안되기 때문에 asdict()사용하여 dictionary로 변경
asdict(localConfig())
# {'BASE_DIR': '/Users/onesixx/my/git/dash-adminlte',
#  'HOST': '127.0.0.1',
#  'PORT': 8085,
#  'DEBUG': True,
#  'DEV_TOOLS_PROPS_CHECK': True}


def ufunc_test(PORT=None, DEBUG=None, **kwargs):
    print(PORT, DEBUG)

ufunc_test(localConfig())
ufunc_test(asdict(localConfig()))
#localConfig(BASE_DIR='/Users/onesixx/my/git/dash-adminlte', 
# HOST='127.0.0.1', PORT=8085, DEBUG=True, DEV_TOOLS_PROPS_CHECK=True) None
# PORT가 localConfig()이라고 생각하고, DEBUG는 없으니 None
ufunc_test(**asdict(localConfig()))
#8085 True

cofig

MENU_ITEMS = ("home", "basiccard", "socialcard", "tabcard",)
menu = [(f"{menu}", f"/{menu}") for menu in MENU_ITEMS]
print(menu)
# [('home',       '/home'),
#  ('basiccard',  '/basiccard'),
#  ('socialcard', '/socialcard'),
#  ('tabcard',    '/tabcard')]

idx=MENU_ITEMS.index("basiccard")
#1
menu[idx]
#('basiccard', '/basiccard')
menu[idx][0]
#'basiccard'
menu[idx][1]
#'/basiccard'

dataclasses 모듈 사용법

https://www.daleseo.com/python-dataclasses/

우리는 파이썬으로 코딩을 하면서 데이터를 담아두기 위해서 여러가지 방법을 사용합니다.

리스트(list), 튜플(tuple), 사전(dictoinary), 네임드 튜플(namedtuple), 세트(set), 프로즌 세트(frozen set)와 같은 내장 자료 구조는 사용하기 간편하다는 장점이 있습니다.

반면에 클래스(class)를 이용해서 데이터를 담아두면 type-safe해지기 때문에 프로그램 실행 중에 오류가 발생할 확률이 적어진다는 장점이 있습니다.

파이썬 3.7에서 dataclasses라는 매우 매력적인 모듈이 표준 라이브러리에 추가되었는데요.
이번 포스팅에서는 내장 자료 구조처럼 편리하면서도 클래스처럼 견고한 데이터 클래스에 대해서 알아보도록 하겠습니다.

기존 방식의 클래스 작성하기

먼저 dataclasses 모듈없이, 우리는 어떻게 데이터를 담아두기 위한 클래스를 작성하였는지 살펴보겠습니다.
예를 들어, 사용자 데이터를 담아두기 위한 User 클래스를 작성한다고 가정하려면 다음과 같은 코드가 필요할 것입니다.

from datetime import date


class User:
    def __init__(self, id: int, name: str, birthdate: date, admin: bool = False) -> None:
        self.id = id
        self.name = name
        self.birthdate = birthdate
        self.admin = admin

        
>>> user = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user
<__main__.User object at 0x105558100>


>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1 == user2
False

위 코드를 잘 살펴보면 보일러 플레이트(boiler-plate)가 눈에 띈다.
idnamebirthdateadmin 각 변수가 3번씩 반복되는 것을 알 수 있습니다. ?

위 클래스의 인스턴스를 출력해보면 출력 결과에 필드값이 나타나지 않아서 불편합니다.

이번에는 이 클래스로 생성한 두 개의 인스턴스를 동등성(equality)을 체크해볼까요?

__repr__() 메서드를 추가하여 필드값이 모두 출력되도록 인스턴스의 출력 형태를 바꿔보도록 하겠습니다.

from datetime import date


class User:
    def __init__(
        self, id: int, name: str, birthdate: date, admin: bool = False
    ) -> None:
        self.id = id
        self.name = name
        self.birthdate = birthdate
        self.admin = admin

    def __repr__(self):
        return (
            self.__class__.__qualname__ + f"(id={self.id!r}, name={self.name!r}, "
            f"birthdate={self.birthdate!r}, admin={self.admin!r})"
        )

    
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1
User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)

두 개의 인스턴스 간에 필드의 값이 모두 같을 때, 동등한 인스턴스로 취급하고 싶다면 __eq__() 메서드를 구현해줘야 합니다.

from datetime import date


class User:
    def __init__(
        self, id: int, name: str, birthdate: date, admin: bool = False
    ) -> None:
        self.id = id
        self.name = name
        self.birthdate = birthdate
        self.admin = admin

    def __repr__(self):
        return (
            self.__class__.__qualname__ + f"(id={self.id!r}, name={self.name!r}, "
            f"birthdate={self.birthdate!r}, admin={self.admin!r})"
        )

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (self.id, self.name, self.birthdate, self.admin) == (
                other.id,
                other.name,
                other.birthdate,
                other.admin,
            )
        return NotImplemented
    
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1 == user2
True

위와 같이 이상적으로 작동하는 클래스를 작성하려면 생각했던 것 보다 많은 양의 코드가 필요하다는 것을 알 수 있습니다.
이러한 보일러 플레이트 코드를 일일이 직접 작성하지 않아도 된다면 얼마나 좋을까요?

데이터 클래스의 기본 기능 (작성하기)

dataclasses 모듈은 위와 같이 데이터를 담아두기 위한 클래스를 매우 적은 양의 코드로 작성하게 해줍니다.
위 예제 코드를 이번에는 dataclasses 모듈을 이용해서 재작성 해보겠습니다.

from dataclasses import dataclass
from datetime import date

@dataclass
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False

>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1
User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)
>>> user2 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1 == user2
True

어떤가요? 너무 간단하지 않은가요? ?

dataclasses 모듈에서 제공하는 @dataclass 데코레이터를 일반 클래스에 선언해주면 해당 클래스는 소위 데이터 클래스가 됩니다.

데이터 클래스는 __init__()__repr__()__eq__()와 같은 메서드를 자동으로 생성해줍니다.
따라서 이 데이터 클래스는 다음과 같이 이전 섹션에서 손수 작성했던 클래스와 동일하게 작동하는 것을 알 수 있습니다.

자, 지금까지 데이터 클래스의 기본 기능을 살펴봤으니,
지금부터 데이터 클래스가 제공하는 더욱 강력한 부가 기능들에 대해서 살펴보겠습니다.

데이터 클래스의 부가 기능

불변 데이터 만들기

기본적으로 데이터 클래스는 담고있는 데이터를 자유 자재로 변경할 수 있습니다.

>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1
User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)

>>> user1.admin = True
>>> user1
User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=True)

만약에 데이터의 불변성(immutability)가 보장되어야 하는 경우라면 다음과 같이 frozen 옵션을 사용하면 됩니다.

from dataclasses import dataclass
from datetime import date

@dataclass(frozen=True)
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False

### 데이터 클래스가 담고 있는 데이터를 변경해보려고 하면 예외가 발생하는 것을 알 수 있습니다.

>>> user1.admin = True
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'admin'       

데이터 대소비교 및 정렬

데이터 클래스의 인스턴스 간에 대소비교를 하려고 하면 다음과 같이 예외가 발생합니다.

>>> user1 < user2
Traceback (most recent call last):
  File "", line 1, in 
TypeError: '<' not supported between instances of 'User' and 'User'

필드값에 따라서 데이터의 대소비교가 필요한 경우라면 order 옵션을 사용할 수 있습니다.

이제 데이터 클래스 간에 대소비교가 가능하고, 따라서 데이터 정렬도 가능해졌습니다.

from dataclasses import dataclass
from datetime import date


@dataclass(order=True)
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False


>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=2, name="Bill Gates", birthdate=date(1955, 10, 28))
>>> user1 < user2
True
>>> user1 > user2
False

>>> sorted([user2, user1])
[User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False),
 User(id=2, name='Bill Gates', birthdate=datetime.date(1955, 10, 28), admin=False)]

세트나 사전에서 사용하기

데이터 클래스의 인스턴스는 기본적으로 hashable하지 않기 때문에,
세트(set)의 값이나 사전(dictionary)의 키로 사용을 할 수 없습니다.

>>> set([user1, user2])
Traceback (most recent call last):
  File "", line 1, in 
TypeError: unhashable type: 'User'
        
        

데이터 클래스의 인스턴스를 hashable하게 만들고 싶다면, unsafe_hash 옵션을 사용하면 됩니다.

이제 세트를 이용해서 중복 데이터를 제거할 수 있습니다.

from dataclasses import dataclass
from datetime import date


@dataclass(unsafe_hash=True)
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False

>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=2, name="Bill Gates", birthdate=date(1955, 10, 28))
>>> user3 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user4 = User(id=2, name="Bixll Gates", birthdate=date(1955, 10, 28))

>>> set([user1, user2, user3, user4])
{User(id=2, name='Bill Gates', birthdate=datetime.date(1955, 10, 28), admin=False), 
 User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)}

데이터 클래스 사용 시 주의사항

데이터 클래스를 사용할 때 흔히 나오는 실수는
list와 같은 가변 데이터 타입의 필드에 기본값을 할당해줄 때 발생합니다.

필드의 기본값은 인스턴스 간에 공유가 되기 때문에 이런 식으로 기본값 할당이 허용되지 않습니다.

from dataclasses import dataclass
from datetime import date
from typing import List


@dataclass(unsafe_hash=True)
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False
    friends: List[int] = []
        
        
ValueError: mutable default  for field friends is not allowed: use default_factory

이럴 때는 dataclasses 모듈에서 제공하는 filed 함수의 default_factory 옵션을 사용해서 매번 새로운 리스트가 생성될 수 있도록 해줘야 합니다.

from dataclasses import dataclass, field
from datetime import date
from typing import List


@dataclass(unsafe_hash=True)
class User:
    id: int
    name: str
    birthdate: date
    admin: bool = False
    friends: List[int] = field(default_factory=list)
        
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1.friends
[]
>>> user1.friends.append(2)
>>> user1.friends
[2]

마치면서

지금까지 파이썬의 dataclasses 내장 모듈을 이용해서 데이터 클래스를 어떻게 작성하고 사용하는지 알아보았습니다.
데이터 클래스를 잘 활용하셔서 보일러 플레이트 코드를 작성할 시간을 아끼고, 그 시간에 좀 더 비지니스에 의미있는 코드를 작성하실 수 있으셨으면 좋겠습니다.

파이썬의 dataclasses 내장 모듈에 대한 더 자세한 내용은 dataclasses - Data Classes - Python 3.8.2 documentation을 참고 바라겠습니다.
dataclasses 모듈이 마음에 드셨다면 이보다 좀 더 강력한 기능을 제공하는 attrs이라는 패키지도 있으니 참고바라겠습니다.

Categories: Python Basic

onesixx

Blog Owner

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x