33. 간단한 구조화 데이터를 위한 데이터 클래스
30장에서는 우리만의 타입을 정의하기 위해 클래스를 만드는 방법을 배웠습니다. 인스턴스를 초기화하기 위해 __init__ 메서드를 작성했고, 인스턴스를 표시하기 위해 __repr__ 메서드를 작성했으며, 비교를 위해 __eq__ 메서드를 작성했습니다. 이 접근 방식은 완벽하게 동작하지만, 특히 클래스가 주로 데이터를 저장하기 위해 존재할 때 반복적인 코드를 많이 작성해야 합니다.
Python의 데이터 클래스(data class) 는 주로 데이터 컨테이너인 클래스를 더 깔끔하고 간결하게 만드는 방법을 제공합니다. @dataclass 데코레이터(decorator)를 사용하면, Python이 여러분이 정의한 클래스 속성에 기반해 __init__, __repr__, __eq__ 같은 공통 메서드를 자동으로 생성합니다. 이렇게 하면 보일러플레이트(boilerplate) 코드가 줄어들고 의도도 더 명확해집니다.
33.1) 데이터 클래스란 무엇이며 언제 사용하나
데이터 클래스(data class) 는 주로 데이터 값을 저장하도록 설계된 클래스입니다. 초기화 및 비교 메서드를 수동으로 작성하는 대신, 클래스에 있어야 할 속성을 정의하면 Python이 필요한 메서드를 자동으로 생성해 줍니다.
데이터 클래스가 중요한 이유
책을 표현하는 일반 클래스를 생각해 봅시다:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, year={self.year})"
def __eq__(self, other):
if not isinstance(other, Book):
return False
return (self.title == other.title and
self.author == other.author and
self.year == other.year)
book1 = Book("1984", "George Orwell", 1949)
print(book1) # Output: Book(title='1984', author='George Orwell', year=1949)
book2 = Book("1984", "George Orwell", 1949)
print(book1 == book2) # Output: True이 코드는 동작하지만, 단지 세 가지 정보를 저장하기 위해 얼마나 많은 코드를 작성했는지 주목해 보세요. __init__, __repr__, __eq__ 메서드는 예측 가능한 패턴을 따릅니다. 즉, 우리가 정의한 속성들을 처리하기만 합니다.
데이터 클래스는 이런 반복을 제거합니다. 특히 다음과 같은 경우에 유용합니다:
- 클래스가 복잡한 동작을 구현하기보다는 주로 데이터를 저장할 때
- 초기화, 문자열 표현, 동등성 비교 같은 표준 메서드가 필요할 때
- 보일러플레이트가 적고 더 명확하고 유지보수하기 쉬운 코드를 원할 때
- 설정 객체(configuration object), 데이터 전송 객체(data transfer object), 단순 레코드를 만들 때
데이터 클래스가 일반 클래스를 대체하는 것은 아닙니다. 서로 보완하는 관계입니다. 사용자 정의 초기화 로직, 복잡한 메서드, 상속 계층이 필요할 때는 일반 클래스를 사용하세요. 관련된 데이터를 위한 구조화된 컨테이너가 주로 필요할 때는 데이터 클래스를 사용하세요.
데이터 클래스와 일반 클래스의 관계
데이터 클래스는 여전히 일반 Python 클래스입니다. 30~32장에서 배운 모든 기능(메서드, 프로퍼티(property), 상속(inheritance), 특수 메서드(special method))을 지원합니다. @dataclass 데코레이터는 단지 공통 메서드 생성을 자동화하여 반복 코드를 작성하지 않아도 되게 해 줍니다.
33.2) @dataclass로 데이터 클래스 만들기
데이터 클래스를 만들려면 dataclasses 모듈에서 dataclass 데코레이터를 임포트하고 클래스 정의에 적용합니다. 클래스 내부에는 클래스가 보관해야 할 데이터를 지정하는 타입 어노테이션(type annotation)이 포함된 클래스 속성을 정의합니다.
기본 데이터 클래스 문법
from dataclasses import dataclass
@dataclass
class Student:
name: str
student_id: int
gpa: float
# 인스턴스 생성
alice = Student("Alice Johnson", 12345, 3.8)
bob = Student("Bob Smith", 12346, 3.5)
print(alice) # Output: Student(name='Alice Johnson', student_id=12345, gpa=3.8)
print(bob) # Output: Student(name='Bob Smith', student_id=12346, gpa=3.5)@dataclass 데코레이터가 하는 일을 살펴봅시다:
-
@dataclass: 이 데코레이터를 적용하면 Python이__init__,__repr__,__eq__메서드를 자동으로 작성해줍니다 -
자동
__init__: Python은 정의된 순서대로 이 세 개의 매개변수를 받아서 인스턴스 속성에 할당하는 초기화 메서드를 생성합니다 -
자동
__repr__: Python은 클래스 이름과 모든 속성 값을 보여주는 문자열 표현을 생성합니다 -
자동
__eq__: Python은 모든 속성을 비교하는 동등성 비교 메서드를 생성합니다 -
타입 annotation을 인스턴스 속성으로 변환: 일반 클래스에서 클래스 본문에
name: str을 쓰면 클래스 속성이 생성됩니다. 하지만@dataclass데코레이터는 이 동작을 변경합니다—이러한 타입 annotation을 사용하여 대신 인스턴스 속성을 정의합니다. 각 인스턴스는 자신만의name,student_id,gpa속성을 갖게 됩니다.
일반 클래스와의 주요 차이점:
# 일반 클래스 - 이것들은 클래스 속성입니다 (모든 인스턴스가 공유)
class RegularStudent:
name: str
student_id: int
# 데이터 클래스 - 이것들은 인스턴스 속성이 됩니다 (각 인스턴스가 자신만의 값을 가짐)
@dataclass
class DataStudent:
name: str
student_id: int데이터 클래스에서 타입 Annotation 이해하기
데이터 클래스에서 타입 annotation은 속성을 정의하고 기대되는 타입을 문서화합니다:
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
in_stock: bool
# 문서화된 대로 올바른 타입 사용
laptop = Product("Laptop", 999.99, True)
print(laptop) # Output: Product(name='Laptop', price=999.99, in_stock=True)
# Python은 타입을 강제하지 않습니다 - 이 코드는 오류 없이 실행됩니다
macbook = Product("Macbook", "expensive", True)
print(macbook) # Output: Product(name='Macbook', price='expensive', in_stock=True)
# 하지만 잘못된 타입을 사용하면 나중에 문제가 발생합니다:
discounted = laptop.price * 0.9 # 작동함: 899.991
discounted = macbook.price * 0.9 # TypeError: can't multiply sequence by non-int of type 'float'
tax = laptop.price + 50 # 작동함: 1049.99
tax = macbook.price + 50 # TypeError: can only concatenate str (not "int") to strPython은 데이터 클래스 인스턴스를 생성할 때 잘못된 타입을 전달하는 것을 막지 않습니다. 타입 annotation은 주로 문서화 역할을 합니다—다른 프로그래머들(그리고 mypy 같은 타입 검사 도구들)에게 어떤 타입을 기대하는지 알려주지만, Python은 런타임에 이를 강제하지 않습니다. 이는 Python의 동적 타이핑 철학과 일치합니다.
하지만 타입 annotation을 따르면 코드가 더 예측 가능해지고 디버깅하기 쉬워집니다. 잘못된 타입을 사용하면 나중에 데이터를 사용할 때 오류가 나타나므로, 버그를 추적하기 어려워집니다. 타입 검사 도구는 코드를 실행하기 전에 이러한 불일치를 잡아낼 수 있어, 문제를 조기에 발견하는 데 도움이 됩니다.
속성에 접근하고 수정하기
데이터 클래스 인스턴스는 일반 클래스 인스턴스와 똑같이 동작합니다. 점 표기법(dot notation)으로 속성에 접근하고 수정합니다:
from dataclasses import dataclass
@dataclass
class Employee:
name: str
position: str
salary: float
emp = Employee("Sarah Chen", "Software Engineer", 95000.0)
# 속성 접근
print(emp.name) # Output: Sarah Chen
print(emp.position) # Output: Software Engineer
# 속성 수정
emp.salary = 100000.0
emp.position = "Senior Software Engineer"
print(emp) # Output: Employee(name='Sarah Chen', position='Senior Software Engineer', salary=100000.0)데이터 클래스는 기본적으로 가변(mutable) 입니다. 생성 후에 속성을 바꿀 수 있습니다. 이는 불변(immutable)인 튜플(tuple)이나 네임드 튜플(named tuple)과는 다릅니다. 불변성이 필요하다면 frozen=True로 데이터 클래스를 설정할 수 있습니다(33.4절에서 살펴보겠습니다).
33.3) 생성되는 메서드: __init__, __repr__, __eq__
@dataclass 데코레이터는 세 가지 핵심 메서드를 자동으로 생성합니다. 이 메서드들이 무엇을 하는지 이해하면 데이터 클래스를 효과적으로 사용할 수 있고, 언제 커스터마이즈해야 하는지도 알 수 있습니다.
생성되는 __init__ 메서드
__init__ 메서드는 제공된 값으로 새 인스턴스를 초기화합니다. Python은 속성 정의 순서를 바탕으로 이를 생성합니다:
from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
# 생성된 __init__은 width와 height를 그 순서대로 받습니다
rect = Rectangle(10.5, 5.0)
print(rect.width) # Output: 10.5
print(rect.height) # Output: 5.0
# 키워드 인자도 사용할 수 있습니다
rect2 = Rectangle(height=8.0, width=12.0)
print(rect2.width) # Output: 12.0
print(rect2.height) # Output: 8.0생성된 __init__은 다음과 같은 코드를 작성한 것과 동일합니다:
def __init__(self, width: float, height: float):
self.width = width
self.height = height이 자동 생성 기능은 반복적인 초기화 코드를 쓰지 않게 해 주며, 특히 속성이 많은 클래스에서 유용합니다.
생성되는 __repr__ 메서드
__repr__ 메서드는 모든 속성 값을 보여주는 인스턴스의 문자열 표현을 제공합니다. 이는 디버깅(debugging)과 로깅(logging)에 매우 유용합니다:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
label: str
point = Point(3.5, 7.2, "A")
print(point) # Output: Point(x=3.5, y=7.2, label='A')
print(repr(point)) # Output: Point(x=3.5, y=7.2, label='A')생성된 __repr__은 객체를 다시 생성하는 데 사용할 수 있을 법한 형식으로 클래스 이름과 모든 속성을 보여주는 관례를 따릅니다. 이는 __repr__ 없이 얻는 기본 표현인 <__main__.Point object at 0x...>보다 훨씬 도움이 됩니다.
생성되는 __eq__ 메서드
__eq__ 메서드는 인스턴스 간 동등성 비교를 가능하게 합니다. 두 데이터 클래스 인스턴스는 대응하는 모든 속성이 같으면 동일하다고 간주됩니다:
from dataclasses import dataclass
@dataclass
class Color:
red: int
green: int
blue: int
color1 = Color(255, 0, 0)
color2 = Color(255, 0, 0)
color3 = Color(0, 255, 0)
print(color1 == color2) # Output: True (같은 RGB 값)
print(color1 == color3) # Output: False (다른 RGB 값)
print(color1 is color2) # Output: False (메모리상 서로 다른 객체)이 자동 동등성 비교는 식별성(identity)이 아니라 값 동등성(value equality) 에 기반합니다. is가 보여주듯 color1과 color2는 메모리에서 서로 다른 객체이지만, 속성이 일치하므로 같다고 간주됩니다.
생성된 __eq__ 메서드는 속성을 정의된 순서대로 비교합니다:
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
year: int
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("1984", "George Orwell", 1949)
book3 = Book("Animal Farm", "George Orwell", 1945)
print(book1 == book2) # Output: True (모든 속성이 일치)
print(book1 == book3) # Output: False (title과 year가 다름)
# Book이 아닌 객체와 비교하면 False를 반환합니다
print(book1 == "1984") # Output: False
print(book1 == None) # Output: False생성된 메서드와 수동 구현 비교하기
데이터 클래스가 제공하는 것을 더 잘 이해하기 위해, 데이터 클래스 버전과 수동 구현을 비교해 보겠습니다:
from dataclasses import dataclass
# 데이터 클래스 버전(간결)
@dataclass
class PersonData:
first_name: str
last_name: str
age: int
# 동일한 수동 버전(장황)
class PersonManual:
def __init__(self, first_name: str, last_name: str, age: int):
self.first_name = first_name
self.last_name = last_name
self.age = age
def __repr__(self):
return f"PersonManual(first_name={self.first_name!r}, last_name={self.last_name!r}, age={self.age})"
def __eq__(self, other):
if not isinstance(other, PersonManual):
return False
return (self.first_name == other.first_name and
self.last_name == other.last_name and
self.age == other.age)
# 둘 다 동일하게 동작합니다
p1 = PersonData("Alice", "Johnson", 30)
p2 = PersonManual("Alice", "Johnson", 30)
print(p1) # Output: PersonData(first_name='Alice', last_name='Johnson', age=30)
print(p2) # Output: PersonManual(first_name='Alice', last_name='Johnson', age=30)데이터 클래스는 반복적인 작업(초기화, 문자열 표현, 비교)을 자동으로 처리하면서도, 위의 온도 변환 메서드처럼 필요한 기능을 자유롭게 추가할 수 있습니다.
데이터 클래스에 사용자 정의 메서드 추가하기
데이터 클래스도 일반 클래스처럼 사용자 정의 메서드를 가질 수 있습니다. @dataclass 데코레이터는 초기화, 표현, 동등성 메서드만 생성해 줄 뿐이며, 다른 기능은 자유롭게 추가할 수 있습니다:
from dataclasses import dataclass
@dataclass
class Temperature:
celsius: float
def to_fahrenheit(self):
"""섭씨 온도를 화씨로 변환합니다."""
return (self.celsius * 9/5) + 32
def to_kelvin(self):
"""섭씨 온도를 켈빈으로 변환합니다."""
return self.celsius + 273.15
def is_freezing(self):
"""온도가 어는점 이하인지 확인합니다."""
return self.celsius <= 0
temp = Temperature(25.0)
print(temp) # Output: Temperature(celsius=25.0)
print(f"{temp.celsius}°C = {temp.to_fahrenheit()}°F") # Output: 25.0°C = 77.0°F
print(f"Kelvin: {temp.to_kelvin()}") # Output: Kelvin: 298.15
print(f"Freezing: {temp.is_freezing()}") # Output: Freezing: False
cold_temp = Temperature(-5.0)
print(f"Freezing: {cold_temp.is_freezing()}") # Output: Freezing: True이처럼 자동 메서드 생성과 사용자 정의 메서드를 조합하면, 공통 작업에는 보일러플레이트를 줄이면서도 도메인 특화 동작에는 완전한 유연성을 확보할 수 있습니다.
33.4) 기본값과 field 옵션
데이터 클래스는 속성에 대한 기본값을 지원하므로, 모든 매개변수를 지정하지 않고도 인스턴스를 생성할 수 있습니다. 또한 field() 함수를 사용하여 비교에서 특정 속성을 제외하거나 문자열 표현에 표시되는 방식을 제어하는 등의 고급 동작을 구성할 수 있습니다.
기본값 제공하기
클래스 정의에서 속성에 직접 기본값을 할당할 수 있습니다. 기본값이 있는 속성은 기본값이 없는 속성 뒤에 와야 합니다:
from dataclasses import dataclass
@dataclass
class User:
username: str
email: str
is_active: bool = True # 기본값
role: str = "user" # 기본값
# 기본값 사용/미사용 인스턴스 생성
user1 = User("alice", "alice@example.com")
print(user1) # Output: User(username='alice', email='alice@example.com', is_active=True, role='user')
user2 = User("bob", "bob@example.com", False, "admin")
print(user2) # Output: User(username='bob', email='bob@example.com', is_active=False, role='admin')
# 키워드 인자로 특정 기본값만 덮어쓰기
user3 = User("charlie", "charlie@example.com", role="moderator")
print(user3) # Output: User(username='charlie', email='charlie@example.com', is_active=True, role='moderator')이 순서 규칙(기본값이 없는 속성 먼저, 기본값이 있는 속성 나중)은 생성되는 __init__ 메서드에서의 모호함을 방지합니다. 이는 20장에서 배운, 기본값이 있는 함수 파라미터가 따라야 하는 요구사항과 같습니다.
가변 기본값이 허용되지 않는 이유
데이터 클래스는 가변 기본값과 관련된 일반적인 실수로부터 보호합니다. 리스트나 딕셔너리 같은 가변 객체를 직접 기본값으로 사용하려고 하면 오류가 발생합니다:
from dataclasses import dataclass
# 이 코드는 오류를 발생시킵니다
@dataclass
class ShoppingCart:
customer: str
items: list = [] # ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory이 오류는 Chapter 20에서 본 함수 기본 인수 문제와 동일한 문제를 방지합니다. 모든 인스턴스가 같은 가변 객체를 공유하게 되는 문제 말이죠.
가변 기본값을 위해 default_factory와 함께 field() 사용하기
해결책은 field() 함수에 default_factory를 사용하는 것입니다. 이렇게 하면 인스턴스마다 새로운 기본값이 생성됩니다:
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
customer: str
items: list = field(default_factory=list) # 올바름: 인스턴스마다 새 리스트
# 이제 각 인스턴스는 자기만의 리스트를 가집니다
cart1 = ShoppingCart("Alice")
cart1.items.append("Book")
print(cart1.items) # Output: ['Book']
cart2 = ShoppingCart("Bob")
print(cart2.items) # Output: [] - Bob은 빈 리스트를 가집니다
cart2.items.append("Laptop")
print(cart1.items) # Output: ['Book'] - Alice의 장바구니는 변경 없음
print(cart2.items) # Output: ['Laptop'] - Bob의 장바구니는 독립적default_factory 매개변수는 해당 속성을 제공하지 않고 인스턴스를 생성할 때마다 새로운 기본값을 생성하기 위해 호출될 함수(list, dict, set 같은)를 받습니다. 예를 들어, default_factory=list는 Python이 각 인스턴스에 대해 list()를 호출하여 새로운 빈 리스트를 생성하도록 합니다.
비교에서 필드 제외하기
때로는 특정 속성을 동등성 비교에서 제외하고 싶을 수 있습니다. 이때 field(compare=False)를 사용합니다:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class LogEntry:
message: str
level: str
timestamp: datetime = field(compare=False) # 타임스탬프는 비교하지 않음
# 같은 메시지이지만 다른 시간을 가진 로그 엔트리 2개 생성
entry1 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 30))
entry2 = LogEntry("User logged in", "INFO", datetime(2024, 1, 15, 10, 35))
# timestamp가 비교에서 제외되므로 동일합니다
print(entry1 == entry2) # Output: True
# 하지만 타임스탬프는 다릅니다
print(entry1.timestamp) # Output: 2024-01-15 10:30:00
print(entry2.timestamp) # Output: 2024-01-15 10:35:00이는 두 인스턴스를 같다고 볼지에 영향을 주면 안 되는 메타데이터 필드(예: 타임스탬프, ID, 내부 카운터)가 있을 때 유용합니다.
표현에서 필드 제외하기
field(repr=False)를 사용하면 문자열 표현에서 필드를 제외할 수도 있습니다:
from dataclasses import dataclass, field
@dataclass
class Account:
username: str
email: str
password: str = field(repr=False) # repr에 password를 표시하지 않음
account = Account("alice", "alice@example.com", "secret123")
print(account) # Output: Account(username='alice', email='alice@example.com')
# password는 표시되지 않지만, 여전히 저장되어 있습니다
print(account.password) # Output: secret123이는 비밀번호, API 키 같은 민감한 데이터나, 표현을 복잡하게 만드는 큰 데이터 구조에 특히 유용합니다.
frozen=True로 데이터 클래스를 불변으로 만들기
기본적으로 데이터 클래스 인스턴스는 가변이므로, 생성 후 속성을 변경할 수 있습니다. 튜플처럼 불변 인스턴스를 원한다면 frozen=True를 사용합니다:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
point = Point(3.0, 4.0)
print(point) # Output: Point(x=3.0, y=4.0)
# 수정을 시도하면 에러가 발생합니다
try:
point.x = 5.0
except AttributeError as e:
print(f"Error: {e}") # Output: Error: cannot assign to field 'x'동결(frozen) 데이터 클래스는 데이터 무결성을 보장하거나, 인스턴스를 딕셔너리 키로 사용하고 싶을 때(딕셔너리 키는 불변이어야 합니다) 유용합니다. 데이터 클래스가 동결되면 Python은 __hash__ 메서드도 생성하여 인스턴스를 해시 가능(hashable)하게 만듭니다:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
# 동결 인스턴스는 딕셔너리 키가 될 수 있습니다
locations = {
Coordinate(40.7128, -74.0060): "New York",
Coordinate(51.5074, -0.1278): "London",
Coordinate(35.6762, 139.6503): "Tokyo"
}
nyc = Coordinate(40.7128, -74.0060)
print(locations[nyc]) # Output: New York33.5) __post_init__로 사용자 정의 초기화하기
때로는 생성된 __init__ 메서드가 실행된 뒤 추가적인 설정이 필요할 수 있습니다. __post_init__ 메서드는 초기화 이후 자동으로 호출되며, 데이터 검증, 파생 속성 계산, 기타 설정 작업을 수행할 수 있습니다.
기본 __post_init__ 사용법
__post_init__ 메서드는 생성된 __init__에 의해 모든 속성이 설정된 뒤 호출됩니다:
from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
area: float = 0.0 # __post_init__에서 계산됩니다
def __post_init__(self):
"""초기화 후 넓이를 계산합니다."""
self.area = self.width * self.height
rect = Rectangle(5.0, 3.0)
print(rect) # Output: Rectangle(width=5.0, height=3.0, area=15.0)
print(f"Area: {rect.area}") # Output: Area: 15.0__post_init__ 메서드는 초기화 중 설정된 모든 인스턴스 속성에 접근할 수 있습니다. 이는 여러 속성에 의존하는 파생 값을 계산할 때 유용합니다.
__post_init__에서 데이터 검증하기
__post_init__의 흔한 용도 중 하나는 제공된 데이터가 특정 요구사항을 만족하는지 검증하는 것입니다:
from dataclasses import dataclass
@dataclass
class BankAccount:
account_number: str
balance: float
def __post_init__(self):
"""계좌 데이터를 검증합니다."""
if self.balance < 0:
raise ValueError("Balance cannot be negative")
# 유효한 계좌
account1 = BankAccount("ACC001", 1000.0)
print(account1) # Output: BankAccount(account_number='ACC001', balance=1000.0)
# 유효하지 않은 계좌 - 잔액이 음수
try:
account2 = BankAccount("ACC002", -500.0)
except ValueError as e:
print(f"Error: {e}") # Output: Error: Balance cannot be negative이 검증을 통해 인스턴스는 항상 유효한 상태를 유지하게 됩니다. 데이터가 요구사항을 충족하지 못하면 인스턴스가 생성되지 않으므로, 프로그램에 유효하지 않은 객체가 존재하는 것을 방지합니다.
field(init=False)와 함께 post_init 사용하기
때로는 __post_init__에서 계산되지만 __init__ 파라미터가 되면 안 되는 속성이 필요할 수 있습니다. 이때 field(init=False)를 사용합니다:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
area: float = field(init=False) # __init__의 파라미터가 아님
circumference: float = field(init=False)
def __post_init__(self):
"""radius로부터 area와 circumference를 계산합니다."""
self.area = math.pi * self.radius ** 2
self.circumference = 2 * math.pi * self.radius
# 초기화 시에는 radius만 필요합니다
circle = Circle(5.0)
print(circle) # Output: Circle(radius=5.0, area=78.53981633974483, circumference=31.41592653589793)
print(f"Area: {circle.area:.2f}") # Output: Area: 78.54
print(f"Circumference: {circle.circumference:.2f}") # Output: Circumference: 31.42이 패턴은 다른 속성들로부터 항상 계산되는 속성이 있고, 초기화 시에 직접 설정되면 안 될 때 유용합니다.
데이터 클래스는 클래스의 모든 힘을 유지하면서도 보일러플레이트를 줄여주는 현대적인 Python 기능입니다. 특히 구조화된 데이터를 다룰 때 깔끔하고 읽기 쉬운 코드를 작성하는 데 큰 가치를 제공합니다. Python 학습을 계속하다 보면, 데이터 중심 프로그래밍 작업에서 데이터 클래스가 자연스러운 선택이 되는 경우를 자주 만나게 될 것이며, 30~32장에서 배운 일반 클래스와도 잘 어울리게 사용할 수 있을 것입니다.