본문 바로가기
공부방/Python & AI

[Python] Annotated

by 래채 2025. 10. 12.

기본 개념

Annotated는 Python 3.9+에서 도입된 타입 힌팅 도구로, 타입에 추가 메타데이터를 붙일 수 있게 합니다.

 
 
python
from typing import Annotated

# 기본 문법
Annotated[타입, 메타데이터1, 메타데이터2, ...]

핵심 특징

  1. 타입 체커는 첫 번째 인자만 확인
    • mypy, pyright 등은 실제 타입만 검사
    • 메타데이터는 타입 검사에 영향 없음
  2. 런타임에서 메타데이터 접근 가능
    • __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

주요 장점

  1. 가독성: 타입과 제약조건을 한 곳에 표현
  2. 재사용성: 공통 타입을 정의하고 재사용
  3. 프레임워크 통합: Pydantic, FastAPI 등과 자연스럽게 통합
  4. 타입 안정성: 기존 타입 체킹 도구와 호환

 

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