포스트

[Python 100일 챌린지] Day 69 - 타입 힌팅과 데이터클래스

[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. 타입 힌팅:
    1
    2
    
    def func(x: int, y: str) -> bool:
        return True
    
  2. typing 모듈:
    • Optional[T]: T 또는 None
    • Union[T1, T2]: T1 또는 T2
    • List[T], Dict[K, V]: 컬렉션 타입
  3. dataclass:
    1
    2
    3
    4
    
    @dataclass
    class Person:
        name: str
        age: int
    
  4. 장점:
    • 코드 가독성 향상
    • 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로 구조화
  • 타입 힌팅으로 안전성 확보
  • 비즈니스 로직도 클래스 안에 포함

📝 오늘 배운 내용 정리

  1. 타입 힌팅: 변수와 함수에 타입 정보를 표시해서 코드를 안전하게 만들어요
  2. typing 모듈: Optional, Union, List 등 복잡한 타입을 표현할 수 있어요
  3. dataclass: 보일러플레이트 코드를 자동 생성해서 클래스 작성을 간단하게 만들어요
  4. 타입 체커: mypy 같은 도구로 실행 전에 타입 오류를 찾을 수 있어요
  5. 실무 활용: FastAPI, Pydantic 등 현대 프레임워크에서 필수로 사용돼요

🔗 관련 자료


📚 이전 학습

Day 68: 고급 컬렉션 (collections 모듈) ⭐⭐⭐

어제는 Counter, defaultdict, deque 등 강력한 컬렉션 타입들을 배웠어요!

📚 다음 학습

Day 70: Phase 7 실전 프로젝트 ⭐⭐⭐

내일은 Phase 7에서 배운 모든 고급 개념을 활용한 실전 프로젝트를 진행해요! 데코레이터, 제너레이터, 타입 힌팅, dataclass를 모두 활용한 멋진 프로젝트를 만들어봐요!


“타입 안전성은 버그를 미리 잡는 최고의 방법이에요!” 🛡️

Day 69/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.