반응형

들어가며

Python에서 TypeVar를 사용할 때마다 헷갈린다면 이 글이 도움이 될 것이다. 문법도 간단하고 원리도 어렵지 않은데, 처음에는 낯설 수 있다. 이 글에서는 TypeVar가 무엇인지, 어떻게 사용하는지, 타입 체커가 어떻게 검사하는지까지 전부 다루어볼 것이다.

목차

  1. TypeVar란 무엇인가?
  2. 실제로 어떻게 사용하는가?
  3. 왜 필요한가?
  4. 문법 정리
  5. 타입 체커는 어떻게 검사하는가?
  6. 인자와 리턴값 모두에서 꼭 사용해야 하는가?
  7. TypeVar 사용의 핵심
  8. 정리

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) 함수를 만들 때 사용한다. 제네릭 함수는 여러 타입을 유연하게 받을 수 있으면서도 타입 안전성을 유지하는 함수이다.

가장 간단한 예시

from typing import TypeVar

T = TypeVar('T')

def get_first_item(items: list[T]) -> T:
    """리스트의 첫 번째 항목을 반환"""
    return items[0]

이 함수의 의미는 다음과 같다:

  • 함수에 들어오는 리스트가 int이면, 반환도 int
  • 함수에 들어오는 리스트가 str이면, 반환도 str
  • 함수에 들어오는 리스트가 dict이면, 반환도 dict

입력 타입과 출력 타입이 같다는 것을 명확히 표현하는 것이다.

왜 필요한가?

TypeVar 없이 작성한다면:

def get_first_item(items: list) -> any:
    return items[0]

이렇게 하면 타입 힌트가 거의 무의미하다. IDE의 자동완성이나 타입 체커가 제대로 도움을 주지 못한다.

TypeVar를 사용하면:

def get_first_item(items: list[T]) -> T:
    return items[0]

타입이 명확해지므로 IDE가 반환 타입을 정확히 알 수 있고, 코드를 작성할 때 훨씬 도움이 된다.

문법 정리

T = TypeVar('T')
  • T = 변수 이름 (관례상 대문자 영어 하나 사용)
  • 'T' = 문자열로 된 이름 (내부적으로 사용됨)

둘이 같은 이름이어야 한다. Python의 관례일 뿐이지만 반드시 지켜야 한다.

타입 체커는 어떻게 검사하는가?

타입 체커(예: mypy)는 코드를 실행하지 않고 타입이 맞는지 검사하는 도구이다.

pip install mypy
mypy your_file.py

검사 방식

from typing import TypeVar

T = TypeVar('T')

def get_first_item(items: list[T]) -> T:
    return items[0]

# ✅ OK - 정수 리스트 입력, 정수 반환
result_int = get_first_item([1, 2, 3])
print(result_int + 10)  # 괜찮음

# ✅ OK - 문자열 리스트 입력, 문자열 반환
result_str = get_first_item(["a", "b", "c"])
print(result_str.upper())  # 괜찮음

# ❌ ERROR - 타입 불일치 감지!
result = get_first_item([1, 2, 3])
print(result.upper())  # mypy가 에러 보고함
# "int"에는 upper() 메서드가 없다고 경고

타입 체커의 논리

  1. 함수 호출 분석: get_first_item([1, 2, 3])를 분석함
  2. T 추론: 리스트가 list[int]이므로, T는 int라고 결정
  3. 반환 타입 확인: T가 int이므로 반환값도 int
  4. 사용처 검증: result.upper()를 확인했는데, int에 upper() 없음 → ❌ 에러 보고

조금 더 복잡한 예시

from typing import TypeVar

T = TypeVar('T')

def swap(a: T, b: T) -> tuple[T, T]:
    """두 값을 바꾸기"""
    return (b, a)

# ✅ OK - 두 매개변수 타입이 같음
result = swap(1, 2)  # T = int

# ❌ ERROR - 두 매개변수 타입이 다름!
result = swap(1, "hello")  # T가 뭐지? int? str? → 에러!

타입 체커는 swap(1, "hello")를 보고 혼란스러워한다:

  • 첫 번째 인자는 int → T는 int여야 함
  • 두 번째 인자는 str → T는 str여야 함
  • 모순! → ❌ 에러 보고

인자와 리턴값 모두에서 꼭 사용해야 하는가?

아니다. 상황에 따라 다르다.

인자만 사용

T = TypeVar('T')

def process(item: T) -> None:
    """입력받은 값을 처리하기만 하고 반환 없음"""
    print(item)

이는 완전히 괜찮다. T는 어떤 타입이든 받을 수 있지만 반환값이 없으므로 T를 리턴에 사용하지 않았다.

리턴값만 사용

T = TypeVar('T')

def create_default(default_value: T) -> T:
    """기본값을 받아서 그대로 반환"""
    return default_value

이것도 문제없다.

같은 T를 여러 곳에 사용

T = TypeVar('T')

def first_and_last(items: list[T]) -> T:
    """리스트의 마지막 항목을 반환"""
    return items[-1]

T는 리스트의 원소 타입반환값의 타입이 같다는 것을 표현한다.

TypeVar 사용의 핵심

TypeVar를 사용하는 목적은:

"이 여러 곳의 타입들이 서로 연관되어 있다"는 것을 표현하는 것

따라서:

  • 같은 타입 관계가 없으면 사용하지 않는 것이 낫고
  • 다른 타입 관계가 있으면 다른 타입 변수를 만들어서 구분한다

나쁜 예시

T = TypeVar('T')

def example(a: T) -> int:  # ❌ 굳이 T를 사용할 이유가 없음
    return len(str(a))

a가 어떤 타입이든 상관없고 무조건 int를 반환하므로 T를 사용할 이유가 없다.

def example(a: object) -> int:  # ✅ 이렇게 명확하게
    return len(str(a))

이것이 훨씬 낫고 명확하다.

정리

  • TypeVar타입 변수이다. 일반 변수가 값을 담듯이 타입을 담는다.
  • 제네릭 함수를 만들 때 사용하여 여러 타입을 유연하게 받으면서도 타입 안전성을 유지한다.
  • 타입 간의 관계를 표현하는 것이 핵심이다. 무조건 둘 다 사용해야 하는 것이 아니라, 의미 있는 관계가 있을 때만 사용한다.
  • 타입 체커는 TypeVar로 표현된 타입의 일관성을 검증하여 런타임 에러를 미리 방지해준다.

TypeVar를 제대로 이해하고 사용하면, 코드의 타입 안전성이 크게 향상되고 유지보수도 훨씬 쉬워진다. 처음에는 낯설 수 있지만 몇 번 사용하면 금방 익숙해질 것이다.

반응형

+ Recent posts