Python typing.Generic 활용법과 제네릭 타입
Python typing 모듈의 Generic을 활용하여 재사용 가능한 타입 안전한 코드를 작성하는 방법을 알아봅니다.
Python typing.Generic 활용법과 제네릭 타입
Python의 typing 모듈은 타입 힌트를 통해 코드의 가독성과 안전성을 높여줍니다. Generic은 여러 타입에서 재사용할 수 있는 타입을 정의할 때 유용합니다.
Environment
$ python --version
Python 3.11.5
$ python -c "import typing; print(typing.__version__ if hasattr(typing, '__version__') else 'built-in')"
built-inProblem: 타입 힌트의 반복
비슷한 구조의 클래스를 여러 타입에 대해 작성할 때 코드가 반복됩니다:
class IntList:
def __init__(self):
self.items: list[int] = []
def add(self, item: int) -> None:
self.items.append(item)
def get(self, index: int) -> int:
return self.items[index]
class StringList:
def __init__(self):
self.items: list[str] = []
def add(self, item: str) -> None:
self.items.append(item)
def get(self, index: int) -> str:
return self.items[index]
# Nearly identical classes, only types differAnalysis: Generic의 동작 원리
from typing import TypeVar, Generic
T = TypeVar('T')
class SimpleContainer(Generic[T]):
def __init__(self):
self.items: list[T] = []
def add(self, item: T) -> None:
self.items.append(item)
def get(self, index: int) -> T:
return self.items[index]
# Type checkers understand these are different types
int_container: SimpleContainer[int] = SimpleContainer()
str_container: SimpleContainer[str] = SimpleContainer()Solution: Generic 활용법
기본 Generic 클래스
from typing import TypeVar, Generic, List, Optional
from dataclasses import dataclass
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')
@dataclass
class Result(Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[str] = None
@classmethod
def ok(cls, data: T) -> 'Result[T]':
return cls(success=True, data=data)
@classmethod
def fail(cls, error: str) -> 'Result[T]':
return cls(success=False, error=error)
# Usage
def divide(a: float, b: float) -> Result[float]:
if b == 0:
return Result.fail("Division by zero")
return Result.ok(a / b)
result: Result[float] = divide(10, 2)
print(result) # Result(success=True, data=5.0, error=None)Multiple Type Parameters
from typing import TypeVar, Generic, Dict, List
K = TypeVar('K')
V = TypeVar('V')
class KeyValueStore(Generic[K, V]):
def __init__(self):
self._store: Dict[K, V] = {}
def set(self, key: K, value: V) -> None:
self._store[key] = value
def get(self, key: K) -> Optional[V]:
return self._store.get(key)
def items(self) -> List[tuple[K, V]]:
return list(self._store.items())
# Different type combinations
int_str_store: KeyValueStore[int, str] = KeyValueStore()
str_int_store: KeyValueStore[str, int] = KeyValueStore()
int_str_store.set(1, "one")
str_int_store.set("one", 1)Generic with Constraints
from typing import TypeVar, Generic
from abc import ABC, abstractmethod
# Constrained TypeVar
Numeric = TypeVar('Numeric', int, float)
class Calculator(Generic[Numeric]):
def __init__(self, value: Numeric):
self.value = value
def add(self, other: Numeric) -> Numeric:
return self.value + other # type: ignore
# Works with int or float
int_calc: Calculator[int] = Calculator(10)
float_calc: Calculator[float] = Calculator(10.5)
print(int_calc.add(5)) # 15
print(float_calc.add(5.5)) # 16.0Generic Inheritance
from typing import TypeVar, Generic, List
T = TypeVar('T')
class BaseRepository(Generic[T]):
def __init__(self):
self._items: List[T] = []
def add(self, item: T) -> None:
self._items.append(item)
def get_all(self) -> List[T]:
return self._items.copy()
class UserRepository(BaseRepository['User']):
def find_by_email(self, email: str) -> Optional['User']:
return next((u for u in self._items if u.email == email), None)
@dataclass
class User:
id: int
name: str
email: str
# Type safe
repo = UserRepository()
repo.add(User(1, "Joel", "joel@example.com"))
user = repo.find_by_email("joel@example.com")Advanced: Protocol과 Generic
from typing import Protocol, TypeVar, Generic
class Comparable(Protocol):
def __lt__(self, other: 'Comparable') -> bool: ...
C = TypeVar('C', bound=Comparable)
class SortedList(Generic[C]):
def __init__(self):
self._items: List[C] = []
def add(self, item: C) -> None:
self._items.append(item)
self._items.sort()
def __iter__(self):
return iter(self._items)
# Works with any comparable type
numbers: SortedList[int] = SortedList()
numbers.add(3)
numbers.add(1)
numbers.add(2)
print(list(numbers)) # [1, 2, 3]
strings: SortedList[str] = SortedList()
strings.add("banana")
strings.add("apple")
strings.add("cherry")
print(list(strings)) # ['apple', 'banana', 'cherry']Lessons Learned
TypeVar로 타입 변수 정의: Generic을 사용하려면 먼저 TypeVar로 타입 변수를 정의해야 합니다.
재사용성 향상: Generic은 동일한 로직을 여러 타입에 대해 재사용할 수 있게 해줍니다.
타입 안전성: Type checker는 Generic을 통해 잘못된 타입 사용을 사전에 방지합니다.
bound 제약: TypeVar에
bound를 지정하면 특정 클래스를 상속한 타입만 허용할 수 있습니다.runtime에는 제한 없음: Python의 Generic은 runtime에 제한을 두지 않으므로, 타입 힌트는 주로 type checker를 위한 것입니다.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.