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 불변 객체 만들기
dataclass
는 frozen=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__
메서드에서 초기화하는 방법을 보여준다. area
는 width
와 height
값을 기반으로 계산된다.
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
필드는 width
와 height
로 계산되기 때문에 직접 생성자에서 값을 전달할 필요가 없다. 따라서 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_percentage
와 salary
는 사용자로부터 입력받지만, total_compensation
은 salary
와 bonus_percentage
에 따라 계산된다. 이때 total_compensation
은 init=False
로 설정하고, __post_init__
에서 이를 계산해준다. 동시에, 생성자에서 설정된 salary
값에 대한 유효성 검사도 __post_init__
에서 수행한다.