파이썬 TypeVar
type annotations
class typing.TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False)
-
TypeVar는 어떨 때 사용하는지, 각 파라미터가 무슨 역할을 하는지 알아보자
def repeat(x: str, n: int) -> list[str]:
"""Return a list containing n references to x."""
return [x] * n
-
위와 같은 함수를 고려해보자
-
입력으로 x
가 들어오면 x
를 n
개 담은 리스트를 반환한다 (참고로 참조라 원소의 id는 동일함)
-
이 때 x
의 타입은 str이길 기대한다
-
물론 str이 아니어도 함수는 문제없이 작동하긴 한다
-
그런데 만약 x
의 타입으로 int도 가능하게 하고 싶으면 어떻게 해야 할까?
def repeat(x: str | int, n: int) -> list[str | int]:
"""Return a list containing n references to x."""
return [x] * n
-
위와 같이 |
또는 typing.Union[str, int]
를 사용해 나타내는 걸 생각해볼 수 있다
-
그런데 이러한 표기법엔 조금의 문제가 존재한다
-
x
타입이 str이라면 반환 타입은 당연히 list[str]
여야 될 것 같지만 list[int]
여도 문제 없다 (mypy같은 타입 검사기에 오류가 발생하지 않음)
-
표기법의 의미 그대로 str 또는 int
이기 때문이다
-
물론 위 함수는 동작상 반환 값의 타입이 list[x]
의 타입을 따라가지만 다른 경우엔 문제가 될 수 있다
def add(a: str | int, b: str | int) -> str | int:
return a + b
-
위의 add
함수를 고려하자
-
a, b, 반환 값의 타입 모두 str이거나 int이라는 첫 번째 경우의 의도와 같이 작성한 것이지만
-
실제론 a와 b의 타입이 달라도 타입 검사기에 문제가 생기지 않는다
-
하지만 둘의 타입이 다르면 실행 시 오류가 발생한다
-
즉, a의 타입이 str이면 b도 str이면 좋겠고 a가 int라면 b도 int이면 좋겠다
-
이러한 소망은 Union
을 사용해선 이룰 수 없다
-
이를 가능하게 하는 것이 TypeVar
이다
def greeting(name: str) -> str:
return "Hello " + name
class MyStr(str): ...
greeting("abc") # Possible
greeting(MyStr("abc")) # Also possible
-
기본적으로 특정 타입의 서브 타입도 허용된다 (name은 str 타입뿐만 아니라 str의 서브 타입도 가질 수 있다)
-
그렇다면 서브 타입이란게 정확히 무엇일까?
-
first_var
가 first_type
타입을 가지고 second_var
가 second_type
타입을 가진다고 해보자
-
first_var
에 second_var
를 할당해도 문제가 없을까? (즉, first_var = second_var
가 문제 없이 가능한가?)
-
다음 두 조건을 만족하면 문제 없이 가능하다고 하자
1.
second_type
의 모든 값은 first_type
의 값 집합에도 존재한다
2.
first_type
의 모든 함수는 second_type
의 함수 집합에도 존재한다
-
이 두 조건을 만족한다면 second_type
은 first_type
의 서브 타입으로 정의된다
-
위 두 조건에 의해 아래 두 문장이 성립한다
1.
모든 타입은 자기 자신의 서브 타입이다
2.
값 집합은 서브 타입으로 대체하는 과정에서 더 작아지지만 함수 집합은 더 커진다
-
직관적인 예시로 Dog 클래스와 Animal 클래스를 생각해보자
-
모든 Dog는 Animal이므로 당연하게도 Dog는 Animal보다 더 많은 함수를 가지고 있다 (2번째 정의)
-
예컨대 Dog가 bark 함수를 가지고 있을 수 있는데 Animal은 bark 함수를 가지고 있지 않다 (모든 Animal이 짖을 수 있는 건 아니다)
- 또 다른 예시
-
int
는 float
의 서브 타입이다 (직관적으로 당연해 보인다)
-
모든 정수는 실수에 속하며 더욱 많은 연산자를 지원한다 (예컨대 비트 쉬프트 연산이 있다)
lucky_number = 3.14 # type: float
lucky_number = 42 # Safe
lucky_number * 2 # This works
lucky_number << 5 # Fails
unlucky_number = 13 # type: int
unlucky_number << 5 # This works
unlucky_number = 2.72 # Unsafe
-
lucky_number는 float 타입이다
-
int는 float의 서브 타입이므로 할당 가능하다
-
하지만 float 타입 변수에는 int 타입 변수에만 적용할 수 있는 비트 쉬프트 연산을 적용할 수 없다
-
unluck_number는 int 타입이다
-
비트 쉬프트 연산은 당연히 작동한다
-
하지만 float은 int의 서브 타입이 아니므로 값을 할당할 수 없다
- 또 다른 예시 2
def append_pi(lst: list[float]) -> None:
lst += [3.14]
my_list = [1, 3, 5] # type: list[int]
append_pi(my_list) # Naively, this should be safe...
my_list[-1] << 5 # ... but this fails
-
int
는 float
의 서브 타입이지만 list[int]
는 list[float]
의 서브 타입이 아니다
-
list[int]
가 list[float]
의 서브 타입이려면 위에서 언급했던 두 조건을 만족해야 한다
-
list[float]
로부터 파생될 수 있는 모든 값은 list[int]
를 포함하므로 첫 번째 조건은 만족한다
-
하지만 append_pi
함수에 list[int]
를 인자로 전달하면 기존의 가능한 연산을 적용할 수 없다
-
my_list
의 타입은 list[int]
이므로 각 원소에 대해 비트 연산이 가능해야 한다
-
하지만 append_pi
함수로 $\pi$를 마지막 원소에 추가하면 해당 원소에 대해 비트 연산이 불가능해진다
-
즉, 원해 가능한 연산자를 적용하지 못해 함수 집합이 오히려 더 작아졌으므로 두 번째 조건을 만족하지 못한다
- 첫 번째 예시
T = TypeVar("T") # Can be anything
def repeat(x: T, n: int) -> list[T]:
return [x] * n
def multiply(x: T, n: int) -> T:
return x * n
-
T = TypeVar("T")
와 같이 변수명과 name 인자명을 동일하게 작성해야 한다
-
이제부터 T
는 함수 내에서 임의의 타입을 의미한다
-
만약 repeat
함수에서 x
의 타입이 int
라면 반환값의 타입은 list[int]
이다
-
참고로 T
의 타입은 동일한 함수내에서만 일치하면 된다
-
repeat 함수에서 T의 타입으로 int를 사용했다고 multiply 함수에서도 int로만 사용해야 된다는 것은 아니다
-
그런데 모든 타입이 가능하게 하기 보단 특정 타입만 가능하게 하고 싶을 수 있다
-
이런 경우엔 제한하고 싶은 타입을 *constraints
로 전달하면 된다
- 두 번째 예시
A = TypeVar("A", str, bytes) # Must be exactly str or bytes
class MyStr(str):
...
def concatenate(x: A, y: A) -> A:
return x + y
def do_nothing(x: A) -> A:
return x
res1 = concatenate("a", "b") # Type of res1 is str
res2 = concatenate(MyStr("a"), "b") # Type of res2 is str
res3 = concatenate(b"a", b"b") # Type of res3 is bytes
res4 = concatenate("a", b"b") # Error, type variable "A" can not be object
res5 = do_nothing(MyStr("ab"))
reveal_type(res5) # Type of res5 is str
-
A = TypeVar("A", str, bytes)
라면 A
의 타입으론 str 또는 bytes만 가능하다
-
여기서 Must be exactly str or bytes
란 표현이 오해를 불러올 수 있다
-
파이썬은 기본적으로 서브 타입을 허용한다
-
즉, A 타입을 가지는 변수에 str의 서브 타입인 MyStr을 대입해도 문제없다
-
대신 str의 서브 타입인 MyStr을 사용하더라도 타입 검사기는 해당 변수의 타입이 MyStr이 아닌 str인 것으로 간주한다
-
res5 = do_nothing(MyStr("ab"))
의 경우 res5
의 타입을 검사하면 MyStr여야 될 것 같지만 실제로는 str이다
-
concatenate("a", b"b")
는 오류를 발생시킨다
-
왜냐하면 str과 bytes의 공통 슈퍼 타입은 object인데 TypeVar를 사용해 str과 bytes만 가능하도록 제한을 걸었기 때문이다
-
한편, 제한하는 타입은 2개 이상부터 가능하다 (예컨대 S = TypeVar("S", str)
과 같이 할 수 없다)
- 세 번째 예시
S = TypeVar("S", bound=str) # Can be any subtype of str
def print_capitalized(x: S) -> S:
print(x.capitalize())
return x
res1 = print_capitalized(MyStr("abc")) # Type of res1 is MyStr
-
대신 bound
를 사용할 수 있다
-
S = TypeVar("S", bound=str)
와 같이 할당하면 S
는 타입으로 str 또는 str의 sub type
을 취할 수 있다
-
constraints와 달리 bound는 서브 타입을 허용한다
-
res1 = print_capitalized(MyStr("abc"))
에서 타입 검사기로 res1의 타입을 검사하면 str이 아닌 MyStr이다
-
하위 타입 말고 정확히 str 타입으로 강제하는 것은 불가능하다 (이런 경우라면 런타입에서 타입이 str인지 검사하자)
- 네 번째 예시
T = TypeVar("T") # Can be anything
class UserID(int):
...
def do_nothing(one_arg: T, other_arg: T) -> None:
pass
do_nothing(1, 2) # Ok, T is int
do_nothing("abc", UserID(42)) # Also Ok, T is object
def do_something1(one_arg: T, other_arg: T) -> None: # Error
print(one_arg.jungsanghwa())
print(other_arg.letsgo())
def do_something2(one_arg: Any, other_arg: Any) -> None: # OK
print(one_arg.jungsanghwa())
print(other_arg.letsgo())
-
T = TypeVar("T")
는 정말 모든 것이 될 수 있을 것 같지만 그렇지는 않다
-
covariant (공변성): 타입이 상속 계층을 따라 동일한 방향으로 변경된다
-
contravariant (반공변성): 타입이 상속 계층을 따라 반대 방향으로 변경된다
-
위 두 파라미터를 사용한 타입 변수는 단독으로는 사용 불가능하고 list
와 같은 제네릭 타입
에만 사용할 수 있다
-
위 두 파라미터는 타입 변수의 파라미터가 아니라 제네릭의 파라미터이다
-
그렇기 때문에 일반 함수에는 사용 불가능하다
T_co = TypeVar("T_co", covariant=True)
def f(x: list[T_co]) -> T_co: # Error
return x[0]
class Animal:
def speak(self) -> str:
return "Animal sound."
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
def speak(self) -> str:
return "Meow!"
class SuperCutyCat(Cat):
def speak(self) -> str:
return "Purr! I'm super cute!"
-
공변성은 상위 타입이 필요한 곳에 하위 타입을 사용할 수 있음을 의미한다
from typing import TypeVar, Generic
Animal_co = TypeVar("Animal_co", bound=Animal, covariant=True)
class AnimalContainer(Generic[Animal_co]):
def __init__(self, animal: Animal_co) -> None:
self.animal = animal
def get_animal(self) -> Animal_co:
return self.animal
dog_container = AnimalContainer(Dog())
cat_container = AnimalContainer(Cat())
# Covariance allows assigning AnimalContainer[Dog] to AnimalContainer[Animal]
animal_container: AnimalContainer[Animal] = dog_container
print(animal_container.get_animal().speak()) # Output: "Woof!"
-
AnimalContainer[Animal]
에 AnimalContainer[Dog]
또는 AnimalContainer[Cat]
도 대입할 수 있다
-
AnimalContainer
는 공변성을 가지므로 AnimalContainer[Dog]
와 AnimalContainer[Cat]
는 AnimalContainer[Animal]
의 서브 타입이기 때문이다
-
반공변성은 상위 타입을 필요로 하는 곳에 하위 타입을 사용할 수 없다는 것을 의미한다
-
대신, 하위 타입이 필요한 곳에 상위 타입을 사용할 수 있다.
Animal_contra = TypeVar("Animal_contra", bound=Animal, contravariant=True)
class AnimalHandler(Generic[Animal_contra]):
def handle(self, animal: Animal_contra) -> None:
print(f"Handling {animal.speak()}")
# Create an instance of a handler that can handle any Animal
wrong_animal_handler: AnimalHandler[Cat] = AnimalHandler[SuperCutyCat]() # Error
animal_handler: AnimalHandler[Cat] = AnimalHandler[Animal]() # Contravariance allows this
# Handle a Cat
animal_handler.handle(Dog()) # Error, Dog is not a subtype of Cat
animal_handler.handle(Animal()) # Error, Animal is not a subtype of Cat
animal_handler.handle(Cat()) # Output: "Woof!"
animal_handler.handle(SuperCutyCat()) # Output: "Purr! I'm super cute!"
-
AnimalHandler[Cat]
에 AnimalHandler[Animal]
도 대입할 수 있다
-
하지만 AnimalHandler[Cat]
에 AnimalHandler[SuperCutyCat]
을 대입할 수는 없다
-
AnimalHandler
는 반공변성을 가지므로 AnimalHandler[SuperCutyCat]
은 AnimalHandler[Cat]
의 서브 타입이 아니기 때문이다
-
int는 float의 서브 타입일 뿐 인스턴스는 아니다
x = 1
print(isinstance(x, float))
# False
-
참고로 bool은 int의 인스턴스이다 (True는 1, False는 0)
x = True
print(isinstance(x, int))
# True
-
bool은 int를 상속 받은 클래스이므로 당연하게도 int의 서브 타입이다
-
int는 float의 서브 타입이고 bool은 int의 서브 타입이므로 bool은 float의 서브 타입이다