Type Annotation, typing, mypy - 더 나은 Python 코드를 위해
Python은 변수의 타입이 언제든지 바뀔 수 있는 동적 타입 언어다.
동적 타입의 문제는 코드가 많아질수록 타입 체크가 힘들어진다는 것인데 Python은 그 문제를 해결하기 위해 Python 3.5에 Type Annotation 기능과 함께 typing
이라는 내장 패키지를 추가했다.
Type Annotation이나 typing
패키지는 동적 타입인 파이썬을 정적 타입으로 만들어주지 않는다. 다만 변수나 함수 파라미터와 반환값이 어떤 타입인지 코드 상에서 명시할 수 있으며, 에디터 레벨에서 경고를 띄워줄 뿐이다.
Type Annotation
아래는 Type Annotation을 적용한 함수다. add 함수의 인자 x, y가 정수형이라는 것과 -> int
표현식을 통해 함수의 반환값이 정수형이라는 것을 알 수 있다.
def add(x: int, y: int) -> int:
return x + y
def add(x: int, y: int) -> int:
return x + y
만약 아래와 같이 add 함수의 인자를 문자열로 전달하면 무슨 일이 일어날까?
print(add('sad', ' machine'))
print(add('sad', ' machine'))
+ 연산자로 두 문자열이 연결되어 잘 출력된다. 어떠한 경고나 오류가 없다. 우리가 원하는 건 이 결과가 아닌데..
$ python3 app.py
sad machine
만약 PyCharm 같은 에디터를 사용한다면 아래와 같이 타입에 대한 경고를 보여준다.
하지만 Python을 쓰는 모두가 PyCharm을 사용하는 것은 아니며, 단순히 경고만 띄워주는 것으로는 동적 타입으로 인해 발생될 수 있는 문제들을 사전에 막기 힘들다. 어떻게 해결해야 할까?
mypy
바로 mypy라는 녀석을 쓰면 된다. pypi에 올라와 있는 패키지이므로 pip을 통하여 간단하게 설치할 수 있다.
pip3 install -U mypy
mypy를 통해 위의 코드를 실행시키면 아래와 같이 타입을 체크하고 문제가 있다면 오류를 발생시킨다. 만약 문제가 없다면 아무것도 출력되지 않는다.
$ mypy app.py
app.py:5: error: Argument 1 to "add" has incompatible type "str"; expected "int"
app.py:5: error: Argument 2 to "add" has incompatible type "str"; expected "int"
typing
만약 int, str과 같은 단순한 타입이 아닌 조금 더 복잡한 타입을 사용한다면 typing
패키지를 사용하면 된다.
만약 정수만 포함하는 리스트를 받는다면 아래와 같이 하면 된다. 딕셔너리(Dict)와 튜플(Tuple)도 가능하다.
from typing import List
def add(x: List[int]) -> int:
return sum(x)
print(add([1, 2, 3]))
from typing import List
def add(x: List[int]) -> int:
return sum(x)
print(add([1, 2, 3]))
Type Aliases
NewType
을 사용하여 타입에 별칭을 붙이는 것도 가능하다. 단순히 별칭을 만드는 것이기 때문에 실제 타입은 원형 타입으로 취급된다.
from typing import List, NewType
UserId = NewType('UserId', int)
user_id = UserId(123)
print(user_id)
# 출력: 123
print(type(user_id))
# 출력: <class 'int'>
IdList = NewType('IdList', List[int])
id_list = [1, 2, 3]
print(IdList(id_list))
# 출력: [1, 2, 3]
print(type(id_list))
# 출력: <class 'list'>
from typing import List, NewType
UserId = NewType('UserId', int)
user_id = UserId(123)
print(user_id)
# 출력: 123
print(type(user_id))
# 출력: <class 'int'>
IdList = NewType('IdList', List[int])
id_list = [1, 2, 3]
print(IdList(id_list))
# 출력: [1, 2, 3]
print(type(id_list))
# 출력: <class 'list'>
Callable
함수 인자에 다른 함수를 넘겨 줄 때는 Callable
를 사용하면 된다. Callable[[인자 타입 리스트], 반환 타입]
형식으로 사용할 수 있다.
from typing import Callable
def add(x: int, y: int) -> int:
return x + y
def subtract(x: int, y: int) -> int:
return x - y
def call_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
return func(x, y)
call_func(10, 20, add)
call_func(10, 20, subtract)
from typing import Callable
def add(x: int, y: int) -> int:
return x + y
def subtract(x: int, y: int) -> int:
return x - y
def call_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
return func(x, y)
call_func(10, 20, add)
call_func(10, 20, subtract)
아래의 경우는 인자로 받는 함수의 반환 타입은 int라 명시되어 있지만, 실제 인자로 받은 함수의 반환 타입이 float이기 때문에 mypy에서 오류를 발생시킨다.
def wrong(x: int, y: int) -> float:
return float(x + y)
def call_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
return func(x, y)
call_func(10, 20, wrong)
def wrong(x: int, y: int) -> float:
return float(x + y)
def call_func(x: int, y: int, func: Callable[[int, int], int]) -> int:
return func(x, y)
call_func(10, 20, wrong)
TypeVar, Union, Optional
TypeVar
를 사용하면 제네릭 타입을 구현할 수 있다. 아래는 모든 요소가 같은 타입으로만 이루어진 Sequence를 전달받아 첫 번째 요소를 반환해주는 예제이다.
from typing import TypeVar, Sequence
T = TypeVar('T')
def get_first_item(l: Sequence[T]) -> T:
return l[0]
print(get_first_item([1, 2, 3, 4]))
print(get_first_item((2.0, 3.0, 4.0)))
print(get_first_item('ABC'))
from typing import TypeVar, Sequence
T = TypeVar('T')
def get_first_item(l: Sequence[T]) -> T:
return l[0]
print(get_first_item([1, 2, 3, 4]))
print(get_first_item((2.0, 3.0, 4.0)))
print(get_first_item('ABC'))
여러 자료 형 중 하나를 받아야 할 때는 TypeVar
에 여러 데이터 타입을 전달해주거나, Union
을 사용하면 된다.
필수적인 인자가 아니라면(항상 값을 전달받지 않아도 된다면) Optional
을 사용한다.
from typing import TypeVar, Union, Optional
Numeric = TypeVar('Numeric', int, float)
Numeric2 = Union[int, float]
OptionalNumeric = Optional[int, float]
# Optional[T]는 Union[T, None]와 같다
from typing import TypeVar, Union, Optional
Numeric = TypeVar('Numeric', int, float)
Numeric2 = Union[int, float]
OptionalNumeric = Optional[int, float]
# Optional[T]는 Union[T, None]와 같다