[Python 100일 챌린지] Day 69 - 타입 힌팅과 데이터클래스
타입 힌팅은 파이썬에 안전벨트를 달아주는 거예요! 🛡️✨
“파이썬은 동적 타이핑이라 자유롭지만, 큰 프로젝트에선 버그가 생기기 쉬워요!” 타입 힌팅을 쓰면 IDE가 자동완성을 도와주고, 타입 체커가 실행 전에 버그를 잡아줘요! dataclass는 보일러플레이트 코드를 확 줄여주는 마법 같은 도구랍니다. FastAPI, Pydantic 같은 현대 프레임워크들은 타입 힌팅을 필수로 사용해요! 😊
전문가처럼 안전하고 깔끔한 코드를 작성하는 방법, 지금 배워봐요!
(35분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Phase 4: 클래스 기초
- Phase 3: 함수 정의
🎯 학습 목표 1: 타입 힌팅 기초 이해하기
한 줄 설명
타입 힌팅 = 코드에 타입 라벨 붙이기 🏷️📝
“이 함수는 정수를 받아서 문자열을 돌려줘요!” 이런 정보를 코드에 표시하는 거예요!
1.1 타입 힌팅이란?
1
2
3
4
5
6
7
8
9
10
# 타입 힌팅 없이
def add(a, b):
return a + b
# 타입 힌팅 사용
def add(a: int, b: int) -> int:
return a + b
print(add(2, 3)) # 5
print(add("Hello", "World")) # HelloWorld (런타임 오류 없음!)
💡 실생활 비유: 타입 힌팅은 도로 표지판 같아요! “여기는 자전거 전용 도로예요”라고 표시하지만, 억지로 차를 몰고 가도 막지는 않죠. 하지만 표지판을 보면 훨씬 안전하게 운전할 수 있어요!
타입 힌팅이 왜 좋을까요? 🌟
- IDE가 자동완성을 완벽하게 해줘요!
- 실행 전에 mypy 같은 도구로 타입 오류를 찾아요
- 코드를 읽는 사람이 의도를 바로 알 수 있어요!
주의: 타입 힌팅은 힌트일 뿐이에요! 파이썬은 런타임에 타입을 강제하지 않아요.
1.2 기본 타입 어노테이션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 변수 타입 어노테이션
name: str = "Alice"
age: int = 25
height: float = 165.5
is_student: bool = True
# 함수 타입 어노테이션
def greet(name: str) -> str:
return f"Hello, {name}!"
def calculate(x: int, y: int) -> int:
return x + y
# 반환값이 없는 함수
def print_message(message: str) -> None:
print(message)
1.3 컬렉션 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import List, Dict, Tuple, Set
# 리스트
numbers: List[int] = [1, 2, 3, 4, 5]
names: List[str] = ["Alice", "Bob", "Charlie"]
# 딕셔너리
scores: Dict[str, int] = {"Alice": 85, "Bob": 92}
# 튜플 (고정 길이)
point: Tuple[int, int] = (10, 20)
# 세트
unique_numbers: Set[int] = {1, 2, 3}
# 함수에서 사용
def get_names() -> List[str]:
return ["Alice", "Bob"]
def get_score(name: str, scores: Dict[str, int]) -> int:
return scores.get(name, 0)
실무에서 이렇게 써요! 💼
- 함수의 입력/출력 타입 명시
- API 응답 데이터 구조 정의
- 데이터베이스 모델 타입 지정
🎯 학습 목표 2: typing 모듈 활용하기
한 줄 설명
typing = 복잡한 타입을 표현하는 도구 🔧📦
“리스트 안의 딕셔너리” 같은 복잡한 타입도 표현할 수 있어요!
2.1 Optional과 Union
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from typing import Optional, Union
# Optional: 값이 있거나 None
def find_user(user_id: int) -> Optional[str]:
"""사용자를 찾으면 이름 반환, 없으면 None"""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
print(find_user(1)) # Alice
print(find_user(99)) # None
# Union: 여러 타입 중 하나
def process_id(user_id: Union[int, str]) -> str:
"""정수나 문자열 ID 처리"""
return f"User ID: {user_id}"
print(process_id(123)) # User ID: 123
print(process_id("abc")) # User ID: abc
💡 실생활 비유: Optional은 “배달 주소 (선택사항)” 같아요! 있어도 되고 없어도 되는 정보를 표현해요. Union은 “신용카드 또는 현금” 같이 여러 옵션 중 하나를 선택하는 거예요!
언제 쓸까요? 🤔
- Optional: 값이 없을 수도 있을 때 (None 가능)
- Union: 여러 타입 중 하나일 때 (A 또는 B)
2.2 Any, Callable, TypeVar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Any, Callable, TypeVar
# Any: 모든 타입 허용
def process_data(data: Any) -> Any:
return data
# Callable: 함수 타입
def apply_func(func: Callable[[int, int], int], x: int, y: int) -> int:
return func(x, y)
def add(a: int, b: int) -> int:
return a + b
print(apply_func(add, 2, 3)) # 5
# TypeVar: 제네릭 타입
T = TypeVar('T')
def first_item(items: List[T]) -> T:
"""리스트의 첫 번째 항목 반환"""
return items[0]
print(first_item([1, 2, 3])) # 1
print(first_item(["a", "b", "c"])) # a
2.3 Literal과 Final
1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Literal, Final
# Literal: 특정 값만 허용
def set_mode(mode: Literal["read", "write", "append"]) -> None:
print(f"Mode: {mode}")
set_mode("read") # ✅
# set_mode("delete") # ❌ 타입 체커가 경고
# Final: 상수 (변경 불가)
MAX_SIZE: Final = 100
# MAX_SIZE = 200 # ❌ 타입 체커가 경고
2.4 실전: API 응답 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Dict, List, Optional, TypedDict
# TypedDict: 딕셔너리 구조 정의
class UserDict(TypedDict):
id: int
name: str
email: str
age: Optional[int]
def get_user(user_id: int) -> UserDict:
"""사용자 정보 반환"""
return {
"id": user_id,
"name": "Alice",
"email": "[email protected]",
"age": 25
}
def get_users() -> List[UserDict]:
"""모든 사용자 반환"""
return [
get_user(1),
get_user(2)
]
typing 모듈의 강력함! ⚡
- 복잡한 타입도 명확하게 표현
- IDE와 타입 체커가 완벽하게 이해
- 코드 문서화 효과!
🎯 학습 목표 3: dataclass로 클래스 간소화하기
한 줄 설명
dataclass = 보일러플레이트 코드 제거 마법 ✨🎩
__init__, __repr__, __eq__ 등을 자동으로 만들어주는 슈퍼 데코레이터예요!
3.1 일반 클래스 vs dataclass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 일반 클래스: 보일러플레이트 코드 많음
class PersonNormal:
def __init__(self, name: str, age: int, city: str):
self.name = name
self.age = age
self.city = city
def __repr__(self):
return f"Person(name={self.name!r}, age={self.age!r}, city={self.city!r})"
def __eq__(self, other):
if not isinstance(other, PersonNormal):
return NotImplemented
return (self.name, self.age, self.city) == (other.name, other.age, other.city)
# dataclass: 간결함!
from dataclasses import dataclass
@dataclass
class PersonData:
name: str
age: int
city: str
# 사용
p1 = PersonData("Alice", 25, "Seoul")
p2 = PersonData("Alice", 25, "Seoul")
print(p1) # PersonData(name='Alice', age=25, city='Seoul')
print(p1 == p2) # True (자동으로 __eq__ 생성됨)
💡 실생활 비유: dataclass는 가구 조립 서비스 같아요! 직접 나사 하나하나 조립하는 대신, 전문가가 한 번에 뚝딱 조립해주는 거죠!
__init__,__repr__,__eq__같은 메서드를 자동으로 만들어줘요!
dataclass가 왜 좋을까요? 🌟
- 코드가 10줄 → 3줄로 줄어들어요!
- 실수할 일이 없어요 (자동 생성)
- 타입 힌팅과 완벽하게 조화돼요!
3.2 dataclass 기능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from dataclasses import dataclass, field
from typing import List
@dataclass
class Student:
name: str
age: int
grades: List[int] = field(default_factory=list) # 기본값
gpa: float = 0.0
def __post_init__(self):
"""객체 생성 후 호출되는 메서드"""
if self.grades:
self.gpa = sum(self.grades) / len(self.grades)
# 사용
student1 = Student("Alice", 20, [85, 90, 95])
print(student1.gpa) # 90.0
student2 = Student("Bob", 21)
print(student2.grades) # [] (빈 리스트)
3.3 dataclass 옵션
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from dataclasses import dataclass, field
# frozen=True: 불변 객체
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
# p.x = 10 # ❌ FrozenInstanceError
# order=True: 비교 연산자 자동 생성
@dataclass(order=True)
class Person:
name: str
age: int = field(compare=False) # 비교에서 제외
sort_index: int = field(init=False, repr=False)
def __post_init__(self):
self.sort_index = self.name
alice = Person("Alice", 25)
bob = Person("Bob", 30)
print(alice < bob) # True (이름 기준 비교)
3.4 실전: 데이터 모델
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
@dataclass
class Product:
id: int
name: str
price: float
category: str
stock: int = 0
tags: List[str] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
@property
def is_available(self) -> bool:
return self.stock > 0
def __str__(self) -> str:
status = "재고 있음" if self.is_available else "품절"
return f"{self.name} - {self.price}원 ({status})"
@dataclass
class Order:
id: int
customer_name: str
products: List[Product] = field(default_factory=list)
total: float = field(init=False)
def __post_init__(self):
self.total = sum(p.price for p in self.products)
# 사용
laptop = Product(1, "노트북", 1500000, "전자제품", stock=10)
mouse = Product(2, "마우스", 30000, "전자제품", stock=50)
order = Order(1, "Alice", [laptop, mouse])
print(f"주문 총액: {order.total:,.0f}원") # 주문 총액: 1,530,000원
🎯 학습 목표 4: 실전 타입 힌팅 예제
예제 1: API 클라이언트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
import requests
@dataclass
class User:
id: int
name: str
email: str
age: Optional[int] = None
class APIClient:
"""타입 힌팅을 적용한 API 클라이언트"""
def __init__(self, base_url: str):
self.base_url: str = base_url
def get_user(self, user_id: int) -> Optional[User]:
"""사용자 정보 가져오기"""
# API 호출 (시뮬레이션)
data = {"id": user_id, "name": "Alice", "email": "[email protected]"}
return User(**data)
def get_users(self) -> List[User]:
"""모든 사용자 가져오기"""
return [
User(1, "Alice", "[email protected]", 25),
User(2, "Bob", "[email protected]", 30)
]
def create_user(self, user: User) -> User:
"""사용자 생성"""
print(f"Creating user: {user.name}")
return user
예제 2: 데이터 검증
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from dataclasses import dataclass
from typing import List
from enum import Enum
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
@dataclass
class Task:
title: str
priority: Priority
completed: bool = False
def mark_done(self) -> None:
self.completed = True
def __str__(self) -> str:
status = "✅" if self.completed else "⬜"
return f"{status} [{self.priority.name}] {self.title}"
class TaskManager:
"""할일 관리자"""
def __init__(self):
self.tasks: List[Task] = []
def add_task(self, task: Task) -> None:
"""할일 추가"""
self.tasks.append(task)
def get_pending_tasks(self) -> List[Task]:
"""미완료 할일 가져오기"""
return [task for task in self.tasks if not task.completed]
def get_high_priority_tasks(self) -> List[Task]:
"""높은 우선순위 할일"""
return [task for task in self.tasks if task.priority == Priority.HIGH]
# 사용
manager = TaskManager()
manager.add_task(Task("코드 리뷰", Priority.HIGH))
manager.add_task(Task("문서 작성", Priority.MEDIUM))
manager.add_task(Task("이메일 확인", Priority.LOW))
for task in manager.get_high_priority_tasks():
print(task)
예제 3: 설정 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from dataclasses import dataclass, field
from typing import Dict, Optional
import json
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
username: str = "admin"
password: str = ""
database: str = "mydb"
@dataclass
class CacheConfig:
enabled: bool = True
ttl: int = 3600 # 초
max_size: int = 1000
@dataclass
class AppConfig:
app_name: str
debug: bool = False
database: DatabaseConfig = field(default_factory=DatabaseConfig)
cache: CacheConfig = field(default_factory=CacheConfig)
@classmethod
def from_json(cls, json_str: str) -> 'AppConfig':
"""JSON에서 설정 로드"""
data = json.loads(json_str)
return cls(**data)
def to_dict(self) -> Dict:
"""딕셔너리로 변환"""
from dataclasses import asdict
return asdict(self)
# 사용
config = AppConfig(
app_name="MyApp",
debug=True,
database=DatabaseConfig(host="db.example.com", port=5432)
)
print(config.database.host) # db.example.com
print(config.cache.enabled) # True
💡 오늘의 핵심 요약
- 타입 힌팅:
1 2
def func(x: int, y: str) -> bool: return True
- typing 모듈:
Optional[T]: T 또는 NoneUnion[T1, T2]: T1 또는 T2List[T],Dict[K, V]: 컬렉션 타입
- dataclass:
1 2 3 4
@dataclass class Person: name: str age: int
- 장점:
- 코드 가독성 향상
- IDE 자동완성 지원
- 타입 체커로 버그 조기 발견
🎯 연습 문제
문제 1: 도서 관리 시스템
타입 힌팅과 dataclass를 사용하여 도서 관리 시스템을 작성하세요.
해답 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
@dataclass
class Book:
isbn: str
title: str
author: str
published_year: int
available: bool = True
@dataclass
class Member:
id: int
name: str
email: str
borrowed_books: List[Book] = field(default_factory=list)
@dataclass
class Library:
name: str
books: List[Book] = field(default_factory=list)
members: List[Member] = field(default_factory=list)
def add_book(self, book: Book) -> None:
self.books.append(book)
def register_member(self, member: Member) -> None:
self.members.append(member)
def find_book(self, isbn: str) -> Optional[Book]:
for book in self.books:
if book.isbn == isbn:
return book
return None
def borrow_book(self, member_id: int, isbn: str) -> bool:
member = next((m for m in self.members if m.id == member_id), None)
book = self.find_book(isbn)
if member and book and book.available:
book.available = False
member.borrowed_books.append(book)
return True
return False
# 테스트
library = Library("시립 도서관")
book1 = Book("123-456", "Python Programming", "John Doe", 2024)
library.add_book(book1)
member1 = Member(1, "Alice", "[email protected]")
library.register_member(member1)
success = library.borrow_book(1, "123-456")
print(f"대출 성공: {success}") # True
print(f"Alice가 빌린 책: {len(member1.borrowed_books)}권") # 1권
출력:
1
2
대출 성공: True
Alice가 빌린 책: 1권
설명:
- namedtuple 대신 dataclass로 구조화
- 타입 힌팅으로 안전성 확보
- 비즈니스 로직도 클래스 안에 포함
📝 오늘 배운 내용 정리
- 타입 힌팅: 변수와 함수에 타입 정보를 표시해서 코드를 안전하게 만들어요
- typing 모듈: Optional, Union, List 등 복잡한 타입을 표현할 수 있어요
- dataclass: 보일러플레이트 코드를 자동 생성해서 클래스 작성을 간단하게 만들어요
- 타입 체커: mypy 같은 도구로 실행 전에 타입 오류를 찾을 수 있어요
- 실무 활용: FastAPI, Pydantic 등 현대 프레임워크에서 필수로 사용돼요
🔗 관련 자료
📚 이전 학습
Day 68: 고급 컬렉션 (collections 모듈) ⭐⭐⭐
어제는 Counter, defaultdict, deque 등 강력한 컬렉션 타입들을 배웠어요!
📚 다음 학습
Day 70: Phase 7 실전 프로젝트 ⭐⭐⭐
내일은 Phase 7에서 배운 모든 고급 개념을 활용한 실전 프로젝트를 진행해요! 데코레이터, 제너레이터, 타입 힌팅, dataclass를 모두 활용한 멋진 프로젝트를 만들어봐요!
“타입 안전성은 버그를 미리 잡는 최고의 방법이에요!” 🛡️
Day 69/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
