database / / 2024. 9. 8. 18:41

[python] dataclass에 대한 설명

Python의 dataclass는 클래스 정의를 간편하게 만들어주는 강력한 도구다. 많은 경우 객체 지향 프로그래밍에서 데이터를 구조화하는 작업은 필수적이다. 하지만 이를 위해 기본적인 클래스 정의에서 반복적인 코드 작성이 필요하다. dataclass는 이러한 번거로움을 줄이고, 코드 가독성을 높여주는 Python 3.7에 도입된 기능이다.

1. dataclass란 무엇인가?

dataclass는 클래스를 정의할 때 데이터를 저장하는 데 중점을 둔 클래스를 간편하게 만들 수 있는 Python의 내장 데코레이터이다. 일반적으로 데이터를 저장하는 클래스는 생성자(_init_), 비교 메서드(_eq_), __repr__ 메서드 등이 반복적으로 작성된다. 하지만 dataclass는 이러한 반복적인 작업을 자동으로 처리해준다.

기본 클래스 정의 예시

먼저, 일반적인 클래스 정의 방법을 살펴보자.

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

    def __repr__(self):
        return f"Person(name={self.name}, age={self.age})"

위 코드에서는 생성자와 __repr__ 메서드를 직접 작성해야 한다. 여기서 dataclass를 사용하면 코드가 훨씬 간결해진다.

__repr__은 객체를 출력할 때 일반적으로 <main.Person object at 0x10a79ffe0>식으로 출력이 되지만 __repr__을 정의하면 여기에 출력된 형식대로 찍힌다.

dataclass로 변환한 코드

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

이렇게 간단하게 작성할 수 있으며, 동일한 기능을 자동으로 제공한다. dataclass는 생성자, __repr__, __eq__ 같은 메서드를 자동으로 생성한다.

2. dataclass의 주요 기능

2.1 기본 기능

dataclass는 다음과 같은 메서드를 자동으로 생성한다.

  • __init__: 생성자 메서드
  • __repr__: 객체의 문자열 표현
  • __eq__: 객체 간의 동등성 비교

예를 들어, Person 클래스를 생성한 후 다음과 같이 사용할 수 있다.

person1 = Person("Alice", 30)
person2 = Person("Bob", 25)

print(person1) 
print(person1 == person2)  # 출력: False

2.2 기본값 설정

dataclass에서 필드의 기본값을 쉽게 설정할 수 있다. 이를 통해 선택적으로 값을 전달하거나 생략할 수 있다.

@dataclass
class Person:
    name: str
    age: int = 20  # 기본값 설정

person1 = Person("Alice")
print(person1)  # 출력: Person(name='Alice', age=20)

2.3 field 함수

때로는 기본값이 복잡하거나 기본값을 dataclass의 생성자가 호출될 때마다 동적으로 설정하고 싶을 때가 있다. 이 경우 dataclasses.field를 사용할 수 있다.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Team:
    name: str
    members: List[str] = field(default_factory=list)  # 빈 리스트를 기본값으로 설정

team1 = Team("Developers")
team1.members.append("Alice")
print(team1)  # 출력: Team(name='Developers', members=['Alice'])

2.4 비교 메서드

dataclass는 기본적으로 __eq__와 같은 비교 메서드를 제공한다. 하지만 더 많은 비교 메서드가 필요할 경우 order=True 옵션을 사용할 수 있다. 이 옵션을 사용하면 <, <=, >, >= 연산자도 사용할 수 있다.

@dataclass(order=True)
class Person:
    name: str
    age: int

person1 = Person("Alice", 30)
person2 = Person("Bob", 25)

print(person1 > person2)  # 출력: True

order의 비교를 위해 sorted() 함수나 list.sort() 메소드를 사용한다. 이때 정렬 기준은 dataclass에 정의된 필드 순서에 따른다. 기본적으로는 첫 번째 필드를 기준으로 비교를 하고, 동일한 값이 있는 경우 그다음 필드를 기준으로 순차적으로 비교힌다.

2.5 불변 객체 만들기

dataclassfrozen=True 옵션을 통해 불변 객체(immutable)를 만들 수 있다. frozen을 활성화하면 객체가 생성된 이후에는 속성을 수정할 수 없다.

@dataclass(frozen=True)
class Person:
    name: str
    age: int

person1 = Person("Alice", 30)
person1.age = 31  # 오류 발생: 'Person' 객체는 불변(frozen)입니다.

3. dataclass의 활용 사례

3.1 JSON 직렬화와 역직렬화

dataclass는 직렬화 라이브러리와 함께 사용할 때 유용하다. dataclass 객체를 JSON으로 변환하거나, JSON 데이터를 객체로 변환하는 데 매우 간편하다.

import json
from dataclasses import dataclass, asdict

@dataclass
class Person:
    name: str
    age: int

# 객체를 JSON으로 변환
person = Person("Alice", 30)
person_json = json.dumps(asdict(person))
print(person_json)  # 출력: {"name": "Alice", "age": 30}

# JSON을 객체로 변환
person_data = json.loads(person_json)
person_obj = Person(**person_data)
print(person_obj)  # 출력: Person(name='Alice', age=30)

3.2 데이터 유효성 검사

dataclass와 함께 Pydantic과 같은 라이브러리를 사용하면 데이터 유효성 검사를 쉽게 추가할 수 있다. dataclass 자체는 기본적인 필드 타입을 제공하지만, 더 복잡한 데이터 검증이 필요할 경우 Pydantic이 매우 유용하다.

4. init=False로 필드를 생성자에서 제외하기

dataclass는 기본적으로 모든 필드를 __init__ 메서드의 인자로 사용한다. 하지만 어떤 필드는 생성자에서 제외하고 싶을 때가 있다. 이때 init=False 옵션을 사용하면 해당 필드를 __init__ 메서드에서 제외할 수 있다. 이 필드는 __post_init__에서 초기화하는 것이 일반적이다.

예시: 계산 필드 설정

아래 예시는 Rectangle 클래스에서 init=False로 설정된 area 필드를 __post_init__ 메서드에서 초기화하는 방법을 보여준다. areawidthheight 값을 기반으로 계산된다.

from dataclasses import dataclass

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = 0.0  # init=False는 자동 초기화를 막음

    def __post_init__(self):
        # __post_init__에서 area를 width와 height로 계산
        self.area = self.width * self.height

# 객체 생성 시 width와 height만 제공하고, area는 자동 계산
rect = Rectangle(5.0, 10.0)
print(rect)  # 출력: Rectangle(width=5.0, height=10.0, area=50.0)

4.1 init=False의 필요성

init=False를 사용하는 이유는 보통 다음과 같다.

  • 필드의 값을 직접 제공하지 않고 다른 필드 값을 기반으로 계산하는 경우.
  • 특정 필드는 초기화 과정에서만 설정되고, 이후에는 값을 변경할 필요가 없는 경우.

위 예시에서 area 필드는 widthheight로 계산되기 때문에 직접 생성자에서 값을 전달할 필요가 없다. 따라서 init=False로 생성자에서 제외하고, __post_init__에서 계산하여 값을 설정한다.

5. __post_init__의 역할

__post_init__dataclass__init__ 메서드가 호출된 후 자동으로 실행되는 메서드이다. 여기에서 init=False로 제외된 필드나 추가적인 초기화 작업을 수행할 수 있다.

예시: 유효성 검사 및 필드 계산

다음은 두 개의 필드를 기반으로 계산 필드를 초기화하면서, 입력 값에 대한 유효성 검사를 동시에 수행하는 예시이다.

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    salary: float
    bonus_percentage: float = 0.0  # 직접 제공하지 않음
    total_compensation: float = 0.0  # 계산 필드, init=False

    def __post_init__(self):
        # 유효성 검사: 급여는 0 이상이어야 함
        if self.salary < 0:
            raise ValueError(f"Salary cannot be negative: {self.salary}")

        # 보너스 퍼센티지를 기반으로 총 보상을 계산
        self.total_compensation = self.salary + (self.salary * self.bonus_percentage / 100)

# 객체 생성
emp = Employee(name="Alice", salary=50000, bonus_percentage=10)
print(emp)  # 출력: Employee(name='Alice', salary=50000, bonus_percentage=10.0, total_compensation=55000.0)

5.1 유효성 검사와 계산 필드

위 예시에서 bonus_percentagesalary는 사용자로부터 입력받지만, total_compensationsalarybonus_percentage에 따라 계산된다. 이때 total_compensationinit=False로 설정하고, __post_init__에서 이를 계산해준다. 동시에, 생성자에서 설정된 salary 값에 대한 유효성 검사도 __post_init__에서 수행한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유