들어가며
Python에서 제네릭 프로그래밍을 할 때 TypeVar와 Generic이 자주 등장한다. 이 두 도구가 무엇이고 어떻게 함께 사용하는지 혼란스러울 수 있다. 이 글에서는 TypeVar와 Generic이 각각 무엇인지, 함수와 클래스에서 어떻게 사용하는지, 그리고 왜 구분해서 사용해야 하는지까지 전부 다루어볼 것이다.
목차
- TypeVar란 무엇인가?
- Generic이란 무엇인가?
- 함수에서의 TypeVar 사용
- 클래스에서의 Generic과 TypeVar
- 함수에서는 Generic을 쓸 필요가 없는 이유
- 클래스에서 Generic이 필요한 이유
- 여러 타입 변수 사용하기
- 타입 체커와 Generic의 중요성
- 정리
TypeVar란 무엇인가?
TypeVar는 타입 변수이다. 일반 변수가 값을 저장하듯이, TypeVar는 타입을 변수처럼 다루게 해준다.
from typing import TypeVar
T = TypeVar('T') # T는 어떤 타입이라도 될 수 있는 타입 변수
변수와의 비유
일반 변수를 살펴보자:
x = 5 # x는 5라는 값을 가짐
x = "hello" # x는 "hello"라는 값으로 변경 가능
TypeVar도 비슷하다:
T = TypeVar('T') # T는 어떤 타입이라도 될 수 있음
차이점은 T는 타입을 담는다는 것이다.
Generic이란 무엇인가?
Generic은 클래스나 함수가 여러 타입을 다룰 수 있도록 만드는 도구이다. TypeVar와 함께 사용하면 더 강력해진다.
Generic의 역할
Generic은 다음과 같은 역할을 한다:
- 클래스나 함수가 제네릭하다는 것을 명시적으로 선언함
- 타입 체커가 타입 관계를 정확히 파악할 수 있도록 함
- 타입 안전성을 강화함
함수에서의 TypeVar 사용
함수에서는 TypeVar만으로 제네릭 기능을 충분히 구현할 수 있다.
from typing import TypeVar
T = TypeVar('T')
def get_first_item(items: list[T]) -> T:
"""리스트의 첫 번째 항목을 반환"""
return items[0]
함수 사용 예시
# 정수 리스트
result = get_first_item([1, 2, 3])
print(result + 10) # 결과: 11
# 문자열 리스트
result = get_first_item(["a", "b", "c"])
print(result.upper()) # 결과: "A"
# 리스트의 리스트
result = get_first_item([[1, 2], [3, 4]])
print(result[0]) # 결과: 1
함수는 호출할 때마다 타입이 자동으로 추론되므로 Generic을 명시적으로 상속할 필요가 없다.
클래스에서의 Generic과 TypeVar
클래스를 제네릭하게 만들려면 Generic을 상속받아야 한다.
from typing import TypeVar, Generic
T = TypeVar('T')
class Box(Generic[T]):
"""어떤 타입이든 담을 수 있는 박스"""
def __init__(self, item: T):
self.item = item
def get(self) -> T:
return self.item
클래스 사용 예시
# 정수를 담는 박스
int_box = Box[int](42)
result = int_box.get() # result는 int
# 문자열을 담는 박스
str_box = Box[str]("hello")
result = str_box.get() # result는 str
# 리스트를 담는 박스
list_box = Box[list]([1, 2, 3])
result = list_box.get() # result는 list
클래스는 인스턴스를 생성할 때 타입을 명시해야 하므로 Generic을 상속받는 것이 필수적이다.
Generic 없이 쓴다면?
from typing import TypeVar
T = TypeVar("T")
class Box:
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.value
box = Box(42) # 이렇게만 쓸 수 있음
이 코드는 실행은 되지만 Box[int] 같은 문법을 사용할 수 없다. 타입 체커도 정확히 검사하지 못한다.
함수에서는 Generic을 쓸 필요가 없는 이유
함수는 이미 TypeVar만으로 제네릭 기능이 충분하다. Generic을 상속할 필요가 없다.
함수의 특성
함수는 호출할 때마다 타입이 자동으로 추론된다:
from typing import TypeVar
T = TypeVar('T')
def process(item: T) -> T:
return item
# 호출할 때마다 타입이 결정됨
result1 = process(42) # T = int
result2 = process("hello") # T = str
result3 = process([1, 2, 3]) # T = list
따라서 Generic을 명시적으로 상속할 필요가 없다. TypeVar만으로 타입 체커가 충분히 추론할 수 있다.
클래스에서 Generic이 필요한 이유
클래스는 객체를 생성할 때 타입을 미리 정해야 한다. 그래서 Generic을 상속받는 것이 필수적이다.
클래스의 특성
클래스는 인스턴스 생성 시 타입을 명시해야 한다:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, item: T):
self.item = item
# 인스턴스 생성 시 타입을 명시
box1 = Box[int](42) # Box의 T는 int로 고정
box2 = Box[str]("hello") # Box의 T는 str로 고정
만약 Generic을 상속하지 않으면:
T = TypeVar('T')
class Box: # ❌ Generic 상속 없음
def __init__(self, item: T):
self.item = item
box = Box[int](42) # ❌ 에러! Box는 제네릭이 아님
box = Box(42) # ✅ 이렇게만 가능
Generic을 상속하지 않으면 Box[int] 같은 문법을 사용할 수 없다.
여러 타입 변수 사용하기
한 클래스나 함수에서 여러 개의 타입 변수를 사용할 수 있다.
클래스에서 여러 타입 변수 사용
from typing import TypeVar, Generic
T = TypeVar('T')
U = TypeVar('U')
class Pair(Generic[T, U]):
"""두 개의 다른 타입을 담을 수 있는 쌍"""
def __init__(self, first: T, second: U):
self.first = first
self.second = second
def get_first(self) -> T:
return self.first
def get_second(self) -> U:
return self.second
사용 예시
# 정수와 문자열의 쌍
pair = Pair[int, str](10, "hello")
num = pair.get_first() # int
text = pair.get_second() # str
# 문자열과 리스트의 쌍
pair2 = Pair[str, list]("items", [1, 2, 3])
name = pair2.get_first() # str
items = pair2.get_second() # list
함수에서 여러 타입 변수 사용
from typing import TypeVar
T = TypeVar('T')
U = TypeVar('U')
def combine(a: T, b: U) -> tuple[T, U]:
"""두 개의 다른 타입 값을 조합"""
return (a, b)
result = combine(42, "hello") # tuple[int, str]
타입 체커와 Generic의 중요성
코드는 Generic 없이도 실행되지만, 타입 체커는 정확히 검사하지 못한다.
예시: Generic 없을 때
from typing import TypeVar
T = TypeVar("T")
class Box:
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.value
box = Box(42)
result = box.get()
print(result + 10) # ✅ 또는 ❌ ?
타입 체커는 혼란스러워한다:
result가int인지 확실하지 않음str일 수도 있고,dict일 수도 있음- 따라서
result + 10이 안전한지 검증할 수 없음
예시: Generic 있을 때
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T):
self.value = value
def get(self) -> T:
return self.value
box = Box[int](42)
result = box.get()
print(result + 10) # ✅ 타입 체커가 안전함을 확인
이제 타입 체커는:
box가Box[int]임을 알 수 있음result가int임을 알 수 있음result + 10이 안전함을 검증할 수 있음
mypy로 검사한 실제 차이
# Generic 없이
box = Box(42)
print(box.get().upper()) # ❌ mypy가 경고하지 않음 (T가 뭔지 모르니까)
# Generic 있이
box = Box[int](42)
print(box.get().upper()) # ❌ mypy가 경고함! (int에는 upper() 없다고)
Generic을 사용하면 타입 체커가 더 정확하게 오류를 발견할 수 있다.
정리
TypeVar와 Generic의 차이
- TypeVar: 타입을 변수처럼 다루는 도구. 함수와 클래스 모두에서 사용 가능.
- Generic: 클래스(또는 함수)가 제네릭하다는 것을 명시적으로 선언. 타입 체커가 정확히 검사할 수 있도록 함.
함수에서의 사용
함수는 TypeVar만으로 충분하다. 호출할 때마다 타입이 자동으로 추론되므로 Generic을 상속할 필요가 없다:
from typing import TypeVar
T = TypeVar('T')
def get_first(items: list[T]) -> T:
return items[0]
클래스에서의 사용
클래스는 Generic을 반드시 상속해야 한다. 인스턴스를 생성할 때 타입을 명시적으로 지정할 수 있도록 하려면 Generic이 필수다:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, item: T):
self.item = item
타입 안전성
- 코드 실행: Generic 없이도 완벽하게 작동
- 타입 검사: Generic을 상속해야 타입 체커가 제대로 검사 가능
- Best Practice: 클래스에서 제네릭을 사용할 땐 항상 Generic을 상속받자
런타임 동작과 타입 체킹은 별개의 문제다. 당신의 코드가 실행되더라도, 개발할 때 타입 체커의 도움을 받으려면 Generic을 올바르게 사용해야 한다.
TypeVar와 Generic을 제대로 이해하고 사용하면, 코드의 타입 안전성이 크게 향상되고 버그를 사전에 방지할 수 있다. 처음에는 복잡해 보일 수 있지만, 몇 번 사용하면 금방 자연스러워질 것이다.
'Python > 문법' 카테고리의 다른 글
| [Python] TypeVar (0) | 2025.11.23 |
|---|---|
| [Python] 비동기(async) 프로그래밍 5 (0) | 2025.05.23 |
| [Python] 비동기(async) 프로그래밍 4 (0) | 2025.05.22 |
| [Python] 비동기(async) 프로그래밍 3 (0) | 2025.05.22 |
| [Python] 비동기(async) 프로그래밍 2 (0) | 2025.05.22 |
