1. 개요

- 파이썬은 소스코드를 한줄 한줄 실행할 때 그때마다 그 코드에서 사용하는 변수의 자료형을 결정하는 동적 타입 언어이며, 따라서 정적 타입 언어와 달리 한 변수를 한 곳에서는 정수형으로 다른 곳에서는 문자열형으로 사용하는 것이 가능하다. (다만, 파이썬은 서로 다른 자료형끼리의 연산 시 에러를 발생시키는 강타입 언어로서 같은 상황에서 암묵적으로 타입을 변환해 연산하는 약타입 언어와는 다르다.) 동적 타입 언어는 개발 과정에서 어떤 변수가 어떤 자료형이어야 할지 사전에 결정하지 않고도 개발을 시작할 수 있다는 등의 장점이 있지만, 코드의 규모가 커지면 개발자의 착오로 서로 다른 자료형끼리 연산이 발생하더라도 어느 부분에서 문제가 발생한지를 찾아내기 어렵다는 등의 단점이 있다.

- 파이썬 3.5에서부터는 파이썬에서도 변수명에 그 변수에 담을 자료형을 명시하는 타입 힌트 문법이 도입되었다. 명시한 자료형과 다른 자료형을 변수에 담는다 해서 에러가 발생하는 것은 아니며 단순히 가독성을 높이기 위한 목적이지만, 특정 IDE는 이를 참조하여 명시한 자료형과 다른 자료형이 변수에 담길 경우 에러 표시를 한다. 또한 작성한 소스코드에 명시한 자료형과 다른 자료형이 담겨있는지를 검사하는 Mypy 같은 외부 유틸리티를 사용해 그 소스코드에 명시한 자료형과 다른 자료형이 있는지를 확인할 수 있다.

2. 간단한 타입 힌트 문법

(1) 변수 선언 시 타입 힌트

1
2
3
num1: int = 48
str1: str = "hello world!"
class1: classA = classA(num1, str1)

(2) 함수에서의 타입 힌트

1
2
3
4
5
def func1(param1: int, param2: str="") -> classA:
    return classA(param1, param2)

def func2(param1: int, param2: str) -> None:
    print(param1, param2)

(3) 클래스의 멤버변수와 메서드의 타입 힌트

1
2
3
4
5
6
class classA:
    m1: int
    m2: str

    def __init__(self, param1: int, param2: str):
        self.m1, self.m2 = param1, param2

3. typing 모듈의 활용

1
import typing

1) 특수 목적 타입 힌트

- 상기한 것과 같은 방식이 아니라 typing 모듈을 import한 후에야 자료형을 명시할 수 있는 경우가 있으며 이는 다음과 같다.

(1) list, set, dict, tuple 객체의 타입 힌트

1
2
3
4
5
list_int1: typing.List[int] = [1, 2, 3]
list_str1: typing.List[str] = ["1", "2", "3"]
dict_str_str1: typing.Dict[str, str] = {"Paul": "UK"}
tuple_int_str1: typing.Tuple[int, str] = (15, "Jack")
set_str1: typing.Set[str] = {"A", "B", "C"}

- 함수의 매개변수로 들어오는 객체가 이를 구성하는 각 원소의 타입은 명확히 알고 있는 iterable 객체라는 사실은 알지만 정확히 어떤 객체인지는 알 수 없다면, 다음 방식으로 추상적으로 표현할 수 있다.

1
2
def func1(param1: typing.Iterable[int]) -> None:
    print(param1)

(2) Final: 추후에 값을 재할당할 수 없도록 하는 타입 힌트

1
pi: typing.Final[int] = 3.1415

(3) Union: 둘 이상 제한된 개수의 자료형이 허용되는 타입 힌트

1
num1: typing.Union[int, float] = 1.5

(4) Optional: None 값을 허용하는 타입 힌트

1
num1: typing.Optional[int] = None # typing.Optional[int]는 사실 typing.Union[int, None]과 같다.

(5) Callable: 람다함수의 타입 힌트

1
2
func1: typing.Callable[[int, str], str] = lambda param1, param2: str(param1)+param2 # Callable[]의 왼쪽 값은 파라미터에 대한 타입 힌트가 되고 오른쪽 값은 리턴값에 대한 타입 힌트가 된다.
func2: typing.Callable[[...], str] = lambda x: str(x) # 파라미터나 리턴값 중 한 쪽에만 타입 힌트를 두고 싶다면 다른 한 쪽에는 ellipsis 객체를 쓸 수 있다.

- 위 표기 방식을 활용해, 매개변수가 콜백함수인 함수에서 타입 힌트를 쓸 수 있다.

1
2
def func1(param1: typing.Callable[[...], str]) -> None:
    print(param1(15))

(6) Any: 모든 유형의 자료형을 허용하는 타입 힌트

1
not_determined1: typing.Any = 3.1415 # Any로 타입이 명시된 변수에는 어떤 자료형이든 다 담을 수 있다.

2) 새로운 자료형 정의

1
2
type_fruits = typing.NewType('type_fruits', str)
fruit1: type_fruits = "사과"

3) Type: 자식 클래스를 허용하는 타입 힌트

1
2
3
4
5
6
7
8
class classA:
    ...

class classB(classA):
    ...

def print_classA(param1: typing.Type[classA]) -> None:
    ...

4. Mypy로 타입 체크하기

Mypy를 설치 후(pip 명령어로 설치 가능하다) 콘솔창에서 다음과 같이 소스파일의 이름과 함께 실행시키면 Mypy가 그 소스파일의 타입 힌트를 통해 자료형 준수 여부를 확인 후 결과를 출력한다.

1
mypy main.py

5. 타입힌트의 단점

- 예를 들어 처음에 ‘정수형만 담을 것’이라고 지정한 변수가 있는데 나중에 보니 그 변수에 실수형을 넣어야 하는 상황이 발생하는 경우가 있을 수 있다. 이 경우에는 그 변수에 대해 처음에 지정했던 타입힌트를 수정해야 하는데, 프로그램이 점점 커지다 보면 이런 부분에서 하나씩 수정하다가 개발 시간이 점점 늘어날 수 있다. 이를 고려하여, 앞으로 어떤 자료형이 변수에 담길지 확실히 알 수 없는 변수에 대해서는 Any 타입힌트를 쓰는 식으로 유연하게 대처할 필요가 있다.