객체 데이터 다루기 (dataclasses, property)

nomad-coders님의 영상을 보다가 dataclasses라는 유용한 라이브러리를 알게 되어 정리하게 되었다. 정리한 내용은 공식 문서 (3.11)를 참고하여 작성하였다. 추가로 객체 데이터를 다룰 때 유용하게 사용하고 있는 property도 함께 소개하였다. 


dataclasses 개념

class Person:
    def __init__(self, pid, name, age):
        self.pid = pid
        self.name = name
        self.age = age

class내에서 __init__을 사용해 필요한 데이터를 저장해두는 아주 전형적인 코드이다. 하지만 받아야 하는 데이터가 많을 경우, 동일한 코드를 반복적으로 작성해야 하는 번거로움이 생긴다. 

이럴 때 dataclasses를 사용해 간결하게 작성할 수 있다. 

from dataclasses import dataclass

@dataclass
class Person:
    pid: int
    name: str
    age: int

대신 Python 3.7부터 표준 라이브러리로 추가되었기 때문에 버전에 유의해야 한다. 


dataclasses 사용법

from dataclasses import dataclass

@dataclass(
    init=True,
    repr=True,
    eq=True,
    order=False,
    unsafe_hash=False,
    frozen=False,
    match_args=True,
    kw_only=False,
    slots=False,
    weakref_slot=False,
)
class Data:
    ...

dataclass의 기본 설정 값이다. 

  • init: True면, __init__ 메서드를 자동 생성한다.
  • repr: True면, __repr__ 메서드를 자동 생성한다. i.e. Person(id=6, name='DeneV')
  • eq: True면, __eq__ 메서드를 생성해 데이터 값들이 일치하는지 확인한다. 
  • frozen: True면, 데이터를 불변값으로 설정한다. 내부 데이터를 변경하려고 시도하면 TypeError가 발생한다. 
  • kw_only: True면, __init__ 메서드는 파라미터로 keyword 인자만 받는다. 그렇지 않으면 TypeError가 발생한다. (3.10에 추가)

 

frozen 예시

@dataclass(frozen=True)
class Person:
    pid: int

me = Person(3)
me.pid = 10  # FrozenInstanceError

kw_only 예시

@dataclass(kw_only=True)
class Person:
    pid: int

me = Person(3)  # TypeError
me = Person(pid=3)  # 성공

field

field는 추가적으로 정보를 설정하기 위해 사용할 수 있다. 

from dataclasses import field

field(
    default=MISSING,
    default_factory=MISSING,
    init=True,
    repr=True,
    hash=None,
    compare=True,
    metadata=None,
    kw_only=MISSING,
)
  • default: 데이터의 기본값을 지정하는데 사용한다. 
  • default_factory: 데이터의 기본값이 필요할 때, 파라미터를 받지 않는 호출 가능 객체로 설정한다. 쉽게 말해, 기본 값을 반환하는 함수 또는 클래스로 선언되어야 한다. defaultdefault_factory는 같이 사용될 수 없다.
  • init: False면, __init__ 메서드의 파라미터에서 제외된다. 
  • repr: False면, __repr__ 메서드의 출력 결과에서 제외된다. 

 

default_factory 예시

from dataclasses import dataclass, field

@dataclass
class Container:
    cid: int
    items: list[int] = field(default_factory=list)

list는 가변 객체이기 때문에 default를 list로 설정하면 참조로 인한 문제가 발생할 수 있다. 따라서 list를 기본 값으로 설정할 때는 반드시 default_factory를 사용해야 한다. 위 예시의 경우, Container가 생성될 때마다 items의 값은 빈 list가 된다. 


__post_init__

post-init은 __init__이 실행된 후, 이어서 초기화를 수행하는 메서드이다.

@dataclass
class Circle:
    radius: int
    circumference: float = field(init=False)

    def __post_init__(self):
        pi = 3.141592
        self.circumference = self.radius * 2 * pi


a = Circle(5)
print(a.circumference)  # 6.283184

먼저 radius가 __init__에서 초기화된 후, __post_init__을 통해 circumference가 초기화되었다. 


타입 변환

from dataclasses import dataclass, asdict, astuple

@dataclass
class Person:
    pid: int
    name: str
    

me = Person(3, "DeneV")
me_tuple = astuple(me)
me_dict = asdict(me)

print(me_tuple)  # (3, 'DeneV')
print(me_dict)  # {'pid': 3, 'name': 'DeneV'}

astupleasdict를 통해 dataclass 객체를 각각 tuple과 dict으로 변환할 수 있다. 


property

property 데코레이터는 gettersetter를 간결하게 작성할 수 있도록 도와준다. getter는 객체 내부의 데이터를 외부에서 접근할 수 있도록 도와주는 메서드이며, setter는 외부에서 객체 내부의 데이터를 수정할 수 있도록 도와주는 메서드이다. getter와 setter를 사용하는 것이 'Pythonic(파이썬스러운)' 코드인지에 대해서는 의견이 나뉘지만, 데이터의 접근·수정 조건이 까다로운 경우라면 불가피하게 getter와 setter를 선언해 주어야 한다. 

@dataclass
class Product:
    __pid: int
    name: str
    
    def get_pid(self):
        """getter"""
        return f"P{self.__pid}_{self.name}" 

    def set_pid(self, pid):
        """setter"""
        if isinstance(pid, int):
            if 0 < pid < 10:
                self.__pid = pid
                return None
        raise TypeError


book = Product(1, "Book")
book.set_pid(7)
book_pid = get_pid()
print(book_pid)  # P7_Book

book.__pid  # AttributeError
book.set_pid(70)  # TypeError

getter는 pid와 name을 조합해 값을 반환하고, setter는 pid가 특정 조건을 만족하는지 검사한 후 등록한다. 하지만 이렇게 작성할 경우, 객체 외부에서 데이터에 접근할 때마다 get_pid와 같이 명시적으로 함수를 호출해야 하기 때문에 코드의 가독성이 떨어진다. 이럴 때 사용할 수 있는 것이 property 데코레이터이다. 

@dataclass
class Product:
    __pid: int
    name: str
    
    @property
    def pid(self):
        """getter"""
        if self.__pid is not None:
            return f"P{self.__pid}_{self.name}"

    @pid.setter
    def pid(self, new_pid):
        """setter"""
        if isinstance(new_pid, int):
            if 0 < new_pid < 10:
                self.__pid = new_pid
                return None
        raise TypeError

    @pid.deleter
    def pid(self):
        """deleter"""
        self.__pid = None


book = Product(1, "Book")

book.pid = 7
print(book.pid)  # P7_Book

del book.pid
print(book.pid)  # None

@propertygetter 메서드를 설정하는데 사용된다. getter 메서드의 이름을 활용해 @'Getter'.setter를 지정하면 setter가 된다. getter와 setter의 이름은 데이터의 변수명과 관계없이 사용할 수 있다. 이렇게 작성하면 객체 외부에서 마치 멤버 변수에 직접 접근하는 것처럼 눈속임하여 사용할 수 있다. 

추가로 deleterdel 키워드를 통해 값을 삭제할 때 호출된다. 위 예시의 경우, pid 값을 삭제하려고 하면 None으로 초기화되도록 작성하였다.