반응형

들어가며

Python에서 제네릭 프로그래밍을 할 때 TypeVarGeneric이 자주 등장한다. 이 두 도구가 무엇이고 어떻게 함께 사용하는지 혼란스러울 수 있다. 이 글에서는 TypeVar와 Generic이 각각 무엇인지, 함수와 클래스에서 어떻게 사용하는지, 그리고 왜 구분해서 사용해야 하는지까지 전부 다루어볼 것이다.

목차

  1. TypeVar란 무엇인가?
  2. Generic이란 무엇인가?
  3. 함수에서의 TypeVar 사용
  4. 클래스에서의 Generic과 TypeVar
  5. 함수에서는 Generic을 쓸 필요가 없는 이유
  6. 클래스에서 Generic이 필요한 이유
  7. 여러 타입 변수 사용하기
  8. 타입 체커와 Generic의 중요성
  9. 정리

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)  # ✅ 또는 ❌ ?

타입 체커는 혼란스러워한다:

  • resultint인지 확실하지 않음
  • 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)  # ✅ 타입 체커가 안전함을 확인

이제 타입 체커는:

  • boxBox[int]임을 알 수 있음
  • resultint임을 알 수 있음
  • 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을 제대로 이해하고 사용하면, 코드의 타입 안전성이 크게 향상되고 버그를 사전에 방지할 수 있다. 처음에는 복잡해 보일 수 있지만, 몇 번 사용하면 금방 자연스러워질 것이다.

반응형

+ Recent posts