고급 타입 힌트
Python의 타입 힌트 시스템은 typing 모듈을 통해 복잡한 타입 관계를 표현할 수 있습니다. 제네릭, TypeVar, Protocol을 활용하면 재사용 가능한 타입 안전 코드를 작성할 수 있습니다.
Optional과 Union
from typing import Optional, Union
# Optional[X]는 Union[X, None]의 단축형
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # 없으면 None 반환
result = find_user(1)
if result is not None:
print(result.upper()) # 타입 좁히기(type narrowing)
# Union: 여러 타입 중 하나
def process(value: Union[int, str, list]) -> str:
if isinstance(value, int):
return f"정수: {value}"
elif isinstance(value, str):
return f"문자열: {value}"
else:
return f"리스트: {len(value)}개 항목"
print(process(42))
print(process("hello"))
print(process([1, 2, 3]))
# Python 3.10+: X | Y 문법 (Union의 단축형)
def greet(name: str | None = None) -> str:
return f"안녕, {name}!" if name else "안녕, 세상!"
print(greet())
print(greet("Alice"))
Literal — 특정 값만 허용
from typing import Literal
# Literal: 특정 상수값만 허용
def set_direction(direction: Literal["north", "south", "east", "west"]) -> None:
print(f"방향 설정: {direction}")
set_direction("north") # OK
# set_direction("up") # 타입 체커 오류
# Literal로 상태 코드 표현
StatusCode = Literal[200, 201, 400, 401, 403, 404, 500]
def create_response(status: StatusCode, body: str) -> dict:
return {"status": status, "body": body}
# 여러 Literal 조합
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
def log(level: LogLevel, message: str) -> None:
print(f"[{level}] {message}")
log("INFO", "서버 시작")
log("ERROR", "데이터베이스 연결 실패")
TypeVar — 제네릭 타입 변수
from typing import TypeVar, Sequence
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
# 타입을 유지하는 함수
def first(items: Sequence[T]) -> T | None:
return items[0] if items else None
# 타입 추론: first([1, 2, 3])은 int를 반환
nums: list[int] = [1, 2, 3]
strs: list[str] = ["a", "b", "c"]
n = first(nums) # n의 타입: int | None
s = first(strs) # s의 타입: str | None
# bound: 상위 타입 제한
from typing import TypeVar
import numbers
N = TypeVar("N", bound=int | float)
def double(x: N) -> N:
return x * 2 # type: ignore
print(double(5)) # 10
print(double(3.14)) # 6.28
# constraints: 허용 타입 명시
AnyStr = TypeVar("AnyStr", str, bytes)
def to_upper(text: AnyStr) -> AnyStr:
return text.upper()
print(to_upper("hello")) # HELLO
print(to_upper(b"hello")) # b'HELLO'
Generic 클래스
from typing import Generic, TypeVar
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
class Stack(Generic[T]):
"""타입 안전한 스택"""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("빈 스택")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("빈 스택")
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
def __repr__(self) -> str:
return f"Stack({self._items})"
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop()) # 2
print(int_stack)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")
print(str_stack.peek())
# 두 타입 매개변수를 가진 제네릭
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
def swap(self) -> "Pair[V, K]":
return Pair(self.value, self.key)
def __repr__(self) -> str:
return f"Pair({self.key!r}, {self.value!r})"
p = Pair("name", "Alice")
swapped = p.swap()
print(p) # Pair('name', 'Alice')
print(swapped) # Pair('Alice', 'name')
Callable 타입 힌트
from typing import Callable
# Callable[[인수타입, ...], 반환타입]
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
print(apply(lambda x, y: x + y, 3, 5)) # 8
print(apply(lambda x, y: x * y, 3, 5)) # 15
# 고차 함수 타입 힌트
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * factor
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# 가변 인수 Callable
from typing import Any
Handler = Callable[..., None] # 임의의 인수, None 반환
def register_handler(event: str, handler: Handler) -> None:
print(f"핸들러 등록: {event}")
handler()
register_handler("click", lambda: print("클릭!"))
Type과 ClassVar
from typing import ClassVar, Type
class Animal:
species_count: ClassVar[int] = 0 # 인스턴스가 아닌 클래스 변수
def __init__(self, name: str) -> None:
self.name = name
Animal.species_count += 1
class Dog(Animal):
tricks: ClassVar[list[str]] = []
def learn_trick(self, trick: str) -> None:
Dog.tricks.append(trick)
# Type[T]: 클래스 자체를 타입 힌트로
def create_animal(cls: Type[Animal], name: str) -> Animal:
return cls(name)
dog = create_animal(Dog, "바둑이")
print(type(dog)) # <class 'Dog'>
print(Animal.species_count) # 1
실전 예제: 타입 안전한 이벤트 시스템
from typing import Callable, Generic, TypeVar, Any
from dataclasses import dataclass, field
EventData = TypeVar("EventData")
@dataclass
class Event(Generic[EventData]):
name: str
data: EventData
Handler = Callable[[Event[Any]], None]
class EventBus:
def __init__(self) -> None:
self._handlers: dict[str, list[Handler]] = {}
def subscribe(self, event_name: str, handler: Handler) -> None:
if event_name not in self._handlers:
self._handlers[event_name] = []
self._handlers[event_name].append(handler)
def publish(self, event: Event[Any]) -> None:
for handler in self._handlers.get(event.name, []):
handler(event)
# 사용 예시
bus = EventBus()
def on_user_created(event: Event[dict]) -> None:
print(f"새 유저 생성: {event.data['name']}")
def on_user_created_email(event: Event[dict]) -> None:
print(f"환영 이메일 발송: {event.data['email']}")
bus.subscribe("user_created", on_user_created)
bus.subscribe("user_created", on_user_created_email)
bus.publish(Event("user_created", {"name": "Alice", "email": "alice@example.com"}))
정리
| 타입 | 용도 | 예시 |
|---|---|---|
Optional[X] | None 가능 | Optional[str] → str | None |
Union[X, Y] | 여러 타입 허용 | Union[int, str] → int | str |
Literal[v] | 특정 값만 허용 | Literal["GET", "POST"] |
TypeVar | 제네릭 타입 변수 | T = TypeVar("T") |
Generic[T] | 제네릭 클래스 | class Stack(Generic[T]) |
Callable[[X], Y] | 함수 타입 | Callable[[int], str] |
ClassVar[T] | 클래스 변수 구분 | ClassVar[int] |
Python 3.9+부터 list[int], dict[str, int] 등 내장 타입을 직접 제네릭으로 사용할 수 있어 typing 임포트가 줄어들고 있습니다.