기본 개념
Annotated는 Python 3.9+에서 도입된 타입 힌팅 도구로, 타입에 추가 메타데이터를 붙일 수 있게 합니다.
python
from typing import Annotated
# 기본 문법
Annotated[타입, 메타데이터1, 메타데이터2, ...]
핵심 특징
- 타입 체커는 첫 번째 인자만 확인
- mypy, pyright 등은 실제 타입만 검사
- 메타데이터는 타입 검사에 영향 없음
- 런타임에서 메타데이터 접근 가능
- __metadata__ 속성으로 접근
- 프레임워크/라이브러리가 활용 가능
사용 케이스
케이스 1: 데이터 검증 (Pydantic)
python
from typing import Annotated
from pydantic import BaseModel, Field
class User(BaseModel):
name: Annotated[str, Field(min_length=2, max_length=50)]
age: Annotated[int, Field(ge=0, le=150)]
email: Annotated[str, Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')]
# 사용
user = User(name="김철수", age=25, email="kim@example.com")
# user = User(name="A", age=25, email="kim@example.com") # 에러: name too short
케이스 2: 문서화
python
from typing import Annotated
# 단위 명시
Height = Annotated[float, "meters"]
Weight = Annotated[float, "kilograms"]
Temperature = Annotated[float, "celsius"]
def calculate_bmi(height: Height, weight: Weight) -> float:
"""BMI 계산"""
return weight / (height ** 2)
# 제약 조건 문서화
PositiveInt = Annotated[int, "must be positive"]
EmailStr = Annotated[str, "valid email format required"]
def send_email(recipient: EmailStr, count: PositiveInt) -> None:
pass
케이스 3: FastAPI - 쿼리 파라미터 검증
python
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Annotated[str | None, Query(max_length=50)] = None,
page: Annotated[int, Query(ge=1, le=100)] = 1,
size: Annotated[int, Query(ge=10, le=100)] = 20
):
return {"q": q, "page": page, "size": size}
케이스 4: 의존성 주입
python
from typing import Annotated
from fastapi import Depends, FastAPI
def get_database():
db = "database_connection"
try:
yield db
finally:
print("Close DB")
app = FastAPI()
@app.get("/users/")
async def get_users(db: Annotated[str, Depends(get_database)]):
return {"db": db, "users": ["user1", "user2"]}
케이스 5: 커스텀 메타데이터 활용
python
from typing import Annotated, get_type_hints, get_args
# 커스텀 메타데이터 클래스
class RangeValidator:
def __init__(self, min_val: int, max_val: int):
self.min_val = min_val
self.max_val = max_val
def validate(self, value: int) -> bool:
return self.min_val <= value <= self.max_val
class Config:
port: Annotated[int, RangeValidator(1, 65535)]
timeout: Annotated[int, RangeValidator(1, 3600)]
# 메타데이터 추출 및 검증
def validate_config(config_class):
hints = get_type_hints(config_class, include_extras=True)
for field_name, field_type in hints.items():
if hasattr(field_type, '__metadata__'):
metadata = field_type.__metadata__
print(f"{field_name}: {metadata}")
# 여기서 검증 로직 실행 가능
validate_config(Config)
# 출력: port: (<__main__.RangeValidator object>,)
# timeout: (<__main__.RangeValidator object>,)
케이스 6: SQLAlchemy - 컬럼 설정
python
from typing import Annotated
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
# 공통 타입 정의
intpk = Annotated[int, mapped_column(primary_key=True)]
str50 = Annotated[str, mapped_column(nullable=False, max_length=50)]
str_optional = Annotated[str | None, mapped_column(nullable=True)]
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[intpk]
username: Mapped[str50]
email: Mapped[str50]
bio: Mapped[str_optional]
케이스 7: 타입 별칭 + 검증
python
from typing import Annotated
# 재사용 가능한 타입 정의
UserId = Annotated[int, "positive integer, unique user identifier"]
Username = Annotated[str, "3-20 chars, alphanumeric + underscore"]
Email = Annotated[str, "valid email format"]
class UserService:
def get_user(self, user_id: UserId) -> dict:
return {"id": user_id}
def create_user(self, username: Username, email: Email) -> UserId:
# 사용자 생성 로직
return 123
메타데이터 접근 방법
python
from typing import Annotated, get_type_hints, get_args
# 타입 정의
Name = Annotated[str, "user's full name", "required"]
# 방법 1: __metadata__ 속성
print(Name.__metadata__) # ("user's full name", "required")
# 방법 2: get_args 사용
args = get_args(Name)
print(args) # (str, "user's full name", "required")
# 방법 3: 클래스에서 추출
class Person:
name: Annotated[str, "full name"]
age: Annotated[int, "in years"]
hints = get_type_hints(Person, include_extras=True)
for field, type_hint in hints.items():
if hasattr(type_hint, '__metadata__'):
print(f"{field}: {type_hint.__metadata__}")
실전 예제: 커스텀 검증 프레임워크
python
from typing import Annotated, get_type_hints
from dataclasses import dataclass
# 검증 규칙 정의
class MinLength:
def __init__(self, length: int):
self.length = length
class MaxLength:
def __init__(self, length: int):
self.length = length
class Range:
def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
# 데이터 모델
@dataclass
class UserForm:
username: Annotated[str, MinLength(3), MaxLength(20)]
password: Annotated[str, MinLength(8)]
age: Annotated[int, Range(0, 150)]
# 검증 함수
def validate(obj):
hints = get_type_hints(type(obj), include_extras=True)
for field_name, field_type in hints.items():
if not hasattr(field_type, '__metadata__'):
continue
value = getattr(obj, field_name)
for validator in field_type.__metadata__:
if isinstance(validator, MinLength):
if len(value) < validator.length:
raise ValueError(f"{field_name} too short")
elif isinstance(validator, MaxLength):
if len(value) > validator.length:
raise ValueError(f"{field_name} too long")
elif isinstance(validator, Range):
if not (validator.min_val <= value <= validator.max_val):
raise ValueError(f"{field_name} out of range")
# 사용
user = UserForm(username="john_doe", password="securepass123", age=25)
validate(user) # 통과
try:
invalid_user = UserForm(username="ab", password="short", age=200)
validate(invalid_user)
except ValueError as e:
print(e) # username too short
주요 장점
- 가독성: 타입과 제약조건을 한 곳에 표현
- 재사용성: 공통 타입을 정의하고 재사용
- 프레임워크 통합: Pydantic, FastAPI 등과 자연스럽게 통합
- 타입 안정성: 기존 타입 체킹 도구와 호환
with Claude
'공부방 > Python & AI' 카테고리의 다른 글
| [AI] ChatPromptTemplate.from_messages (0) | 2025.10.13 |
|---|---|
| [Python] Sequence (0) | 2025.10.12 |
| [AI] add_messages (0) | 2025.10.12 |
| [AI] MessagesPlaceholder (0) | 2025.10.12 |
| [AI] ChatPromptTemplate (0) | 2025.10.12 |