💻 TIL
Fluent Python 1. 데이터 구조체 - (5) 데이터 클래스 빌더
생성 일시
Feb 15, 2025 11:27 PM
CHAPTER 5 데이터 클래스 빌더
데이터 클래스는 데이터를 저장하는 객체를 정의하는 데에 초점을 맞춘 클래스
따라서, 로직을 수행하기 보다 속성(필드)들을 담고 있는 컨테이너에 가까움
collections.namedtuple
- Python 2.6 도입된 튜플 기반의 불변 데이터 구조
typing.NamedTuple
- Python 3.5 도입된 튜플 기반의 불변 데이터 구조
- 타입 힌트 지원! 따라서 속성에 타입을 지정 가능
dataclasses.dataclass
- Python 3.7 도입된 클래스 기반의 가변 데이터 구조 (
frozen=True
로 불변가능)
- 클래스 기반이니 관련 메서드 정의 가능. 독스트링도 쉽게 추가 가능
주요 기능
- 딕셔너리 생성:
__dict__
from dataclasses import dataclass @dataclass class Person: name: str age: int # 인스턴스 생성 person = Person(name="Nara", age=90) # 딕셔너리로 변환 person_dict = person.__dict__ print(person_dict) # 출력: {'name': 'Nara', 'age': 90}
- 필드명 및 기본값 가져오기:
dataclasses.fields()
from dataclasses import dataclass, fields @dataclass class Person: name: str age: int = 30 # 기본값 설정 # 필드 정보 가져오기 for field in fields(Person): print(f"Field name: {field.name}, Type: {field.type}, Default: {field.default}") # 출력 # Field name: name, Type: <class 'str'>, Default: <dataclasses._MISSING_TYPE object at 0x7fbdde0c87c0> # Field name: age, Type: <class 'int'>, Default: 30
참고 - 기본값이 지정되지 않으면
__MISSING TYPE
으로 나옴- 필드형 가져오기
from dataclasses import dataclass @dataclass class Person: name: str age: int # 필드형 가져오기 print(Person.__annotations__) # 출력 # {'name': <class 'str'>, 'age': <class 'int'>}
- 속성을 변경해 인스턴스 새로 만들기:
__class__
from dataclasses import dataclass @dataclass class Person: name: str age: int # 인스턴스 생성 person1 = Person(name="Nara", age=90) # 속성 변경 후 새로운 인스턴스 생성 person2 = person1.__class__(name="Nara", age=91) print(person1) # Person(name='Nara', age=90) print(person2) # Person(name='Nara', age=91)
- 실행 시 새 클래스 생성: 클래스 정의가 실행될 때 동적으로 클래스가 생성됨. 따라서 클래스를 동적으로 정의도 가능
@dataclass 데코레이터 이용
from dataclasses import dataclass # 실행 시 새로운 클래스 정의 def create_person_class(): @dataclass class Person: name: str age: int return Person # 새로운 클래스 생성 Person = create_person_class() # 인스턴스 생성 person = Person(name="Nara", age=90) print(person) # Person(name='Nara', age=90)
자료형 어노테이션
변수나 함수의 파라미터, 반환값에 대해 예상되는 자료형을 명시적으로 지정하는 방법.
# 변수에 타입 힌트 x: int = 10 # x는 정수 타입 # 함수에 타입 힌트 def add(a: int, b: int) -> int: return a + b
⁉️실행 시 효력이 없다
자료형 어노테이션은 실행시에 아무런 영향을 미치지 않음. Python은 동적 타입 언어이기 때문에, 변수나 함수에 지정된 타입힌트는 실행 중 타입을 강제하지 않으며 타입 체크를 자동으로 수행하지도 않음.
그럼 왜? 코드 분석과 문서화의 도구로 활용이 가능하고 가독성이 올라감 💪
변수 어노테이션 구문
# 정수 타입 변수 age: int = 30 # 문자열 타입 변수 name: str = "Alice" # 리스트 타입 변수 numbers: list[int] = [1, 2, 3]
디스크립터(Descriptor)
- Python에서 객체의 속성에 접근하거나 설정할 때 특별한 동작을 구현할 수 있도록 하는 방법.
- 속성의 접근과 수정을 제어할 수 있는 특별한 객체 (읽기/쓰기/삭제 동작 제어)
__get__
,__set__
,__delete__
메서드를 구현하여 속성에 대한 동작을 정의
5.6 @dataclass
추가 설명
시그니처 | 기본값 | 설명 |
init | True | __init__ 메서드 자동 생성 여부 |
repr | True | __repr__ 메서드 자동 생성 여부 |
eq | True | __eq__ 메서드 자동 생성 여부 |
order | False | 비교 연산자( < , <= , > , >= ) 자동 생성 여부 |
unsafe_hash | False | __hash__ 메서드 자동 생성 여부 (주의: 불변 객체여야 함) |
frozen | False | 불변 객체로 만들어 속성 값을 변경할 수 없게 할지 여부 |
5.6.1 필드 옵션 dataclasses.field()
각 속성에 대해 세부 동작 설정하는 방법.
dataclass
의 기본 동작을 변경하거나, 개별 필드에 특화된 설정을 제공옵션 | 기본값 | 설명 |
default | _MISSING_TYPE | 필드의 기본값을 설정 |
default_factory | _MISSING_TYPE | 동적 기본값을 설정 (예: 빈 리스트, 딕셔너리 등) |
init | True | __init__ 에서 필드를 포함시킬지 여부 |
repr | True | __repr__ 에서 필드를 포함시킬지 여부 |
compare | True | 비교 연산에서 필드를 포함시킬지 여부 ( == , < 등) |
hash | True | __hash__ 메서드에서 필드를 포함시킬지 여부 |
metadata | 없음 | 필드에 대한 추가 정보를 제공 (딕셔너리 형식) |
kw_only | False | 키워드 인자만 받도록 설정 |
신경 쓰이는 옵션만 살펴보기 😳
default_factory
- 기본값을 함수나 객체 생성을 통해 동적으로 설정할 때. (예를 들어, 빈 리스트나 딕셔너리를 기본값으로 설정하고 싶을 때)
-
default
는 고정된 값일 때.default_factory
는 동적인 값 일때
from dataclasses import dataclass, field from typing import List @dataclass class Person: name: str hobbies: List[str] = field(default_factory=list) # 동적 기본값 person1 = Person(name="Nara") person2 = Person(name="John") person1.hobbies.append("Reading") person2.hobbies.append("Music") print(person1.hobbies) # ['Reading'] print(person2.hobbies) # ['Music']
init
- False로 처리 시에 해당 필드는
__init__
메서드에 포함되지 않음. 외부에서 직접 값을 설정해야함
from dataclasses import dataclass, field @dataclass class Person: name: str age: int = field(init=False) # __init__에서 제외 person = Person(name="Nara") person.age = 30 # __init__에서 age는 제외되었으므로, 직접 설정 print(person.age) # 30
compare
- False로 처리 시에 해당 필드는 비교 연산에 포함되지 않음. (eq, order 설정 무시)
from dataclasses import dataclass, field @dataclass class Person: name: str age: int = field(compare=False) # 비교에서 제외 person1 = Person(name="Nara", age=90) person2 = Person(name="Nara", age=90) print(person1 == person2) # True (age는 비교되지 않음)
kw_only
- 해당 필드가 키워드 인자로만 받을 수 있도록 설정. True일 경우,
__init__
메서드에서 키워드 인자로만 제공받을 수 있음
키워드 인자란?
- 함수 호출 시에 인자의 이름을 명시하여 값을 전달하는 방식!
예를 들어,
name = “Nara”
처럼 매개변수의 이름을 지정하고 값을 전달하는 방식from dataclasses import dataclass, field @dataclass class Person: name: str age: int = field(kw_only=True) # age는 키워드 인자만 받도록 설정 # 키워드 인자 방식으로 값 전달 person = Person(name="Nara", age=90) # 포지셔널 인자 방식으로 값 전달 시 에러 발생 # person = Person("Nara", 90) # TypeError: non-keyword argument 'age' passed to field 'age'
5.7 코드 악취로서의 데이터 클래스 🤧
‣
객체지향 프로그램의 핵심 개념은 데이터와 행위를 클래스라는 하나의 단위에 통합하는 것. 클래스가 널리 쓰이지만, 그 자체로 의미 있는 작업을 수행하지 않는다면 그 인스턴스를 다루는 코드가 시스템에 산재한 메서드와 함수에 분산될 수 있다.
그럼 언제 써요?
스캐폴딩으로서의 데이터 클래스
- 프로젝트, 모듈 새로 시작 전에 간단하게 뼈대 만들기용
중간 표현으로서의 데이터 클래스
- JSON이나 기타 교환 포맷으로 Export될 레코드를 만들거나 시스템 경계를 넘어 방금 Import 된 데이터를 보관 하는 데에 도움 (데이터 직렬화)
5.8 클래스 인스턴스 패턴 매칭
Python 3.10에서 패턴 매칭 기능(
match/case
)도입 이후, 클래스 인스턴스를 대상으로 패턴 매칭 활용하는 방법1. 단순 클래스 패턴
- 클래스 이름만 사용. 객체가 해당 클래스의 인스턴스인지 확인
class Person: def __init__(self, name, age): self.name = name self.age = age def match_class(obj): match obj: case Person(): # Person 클래스의 인스턴스인지 확인 print("This is a Person instance") case _: print("This is not a Person instance") # 인스턴스 생성 p = Person(name="Nara", age=90) match_class(p) # 출력: This is a Person instance
2. 키워드 클래스 패턴
- 클래스의 속성이나 필드를 특정 키워드로 매칭. 객체의 속성이 특정 키워드를 만족하는지 여부 판단 가능
class Person: def __init__(self, name, age): self.name = name self.age = age def match_class(obj): match obj: case Person(name=name, age=age): # 이름과 나이를 키워드로 매칭 print(f"This is a Person with name: {name} and age: {age}") case _: print("This is not a Person instance") # 인스턴스 생성 p = Person(name="Nara", age=90) match_class(p) # 출력: This is a Person with name: Nara and age: 90
3. 위치 클래스 패턴
- 객체가 여러 개의 위치 인자를 가진 클래스일 때, 위치 인자에 대한 매칭을 수행하는 방식
class Person: def __init__(self, name, age): self.name = name self.age = age def match_class(obj): match obj: case Person(name, age): # 위치 인자에 매칭 print(f"This is a Person with name: {name} and age: {age}") case _: print("This is not a Person instance") # 인스턴스 생성 p = Person(name="Nara", age=90) match_class(p) # 출력: This is a Person with name: Nara and age: 90
그런데 책은 왜… 갑자기 클래스 인스턴스 패턴 매칭 얘기를 했지?
.jpg?table=block&id=19cb4bcc-3d37-80ee-903d-e7209b48b6e9&cache=v2)
책의 어느 부분에도 자연스럽게 녹아 들기 어려운 부분이라서 그랬던 걸까?
아니면 데이터 클래스 필드 옵션에서 kw_only 같은 키워드 인자 옵션을 설명할 때 사실 너무너무나 말하고 싶었던걸까? 알 수가 없는 저자의 의식의 흐름… 🤷♂️
그래도 같은 챕터에 나와있으니 합쳐보자
from dataclasses import dataclass @dataclass class Person: name: str age: int @dataclass class Product: product_name: str price: float def process_data(obj): match obj: case Person(name=name, age=age): print(f"Person: {name}, Age: {age}") case Product(product_name=name, price=price): print(f"Product: {name}, Price: {price}") case _: print("Unknown data type") person = Person(name="Nara", age=90) product = Product(product_name="Laptop", price=1200.0) process_data(person) # Person: Nara, Age: 90 process_data(product) # Product: Laptop, Price: 1200.0
- 서로 다른 데이터 클래스를 만들어 보고
- 해당 클래스의 인스턴스를 만들어서
- 그를 패턴 매칭으로 처리해보기!