Programming/Python

[Python] 타이핑 / 타입 힌트 (typing / type hint)

Jonghyuk Baek 2021. 4. 12. 02:08

Introduction

파이썬은 기본적으로 동적 타입 언어 (dynamically typed language) 이다.

 

이 말은 결국 어떤 변수에 값을 대입할 때, 타입을 선언해 줄 필요가 없다는 말이다 (프로그램 실행 중에 타입이 결정됨). 이런 특성은 C++이나 Java 등 변수를 선언할 때 부터 타입을 strict 하게 지정해 줘야 하는 정적 타입 언어들과 비교된다.

 

컴파일 이후에 유연하게 타입이 결정된다는 특성 덕분에 파이썬은 빠른 개발이 가능하며 언어 자체의 진입 장벽이 낮다는 장점이 있지만, 타입 안전성이 컴파일 단계에서 보장되지 않아 실행 중 예상치 못한 TypeError와 마주할 수 있다는 단점도 있다. 


Type hint

이런 문제에 대응하기 위해 Python 3.5 버전 이후부터 기본 모듈로 typing 모듈이 추가되어, 타입 힌트 (type hint) 기능을 이용할 수 있게 되었다. 한번 다음 예제를 보자.

def subtract(var_a, var_b):
    return var_a - var_b

subtract(3, '5')

>>
TypeError: unsupported operand type(s) for -: 'int' and 'str'

위 코드는 코드 작성 단계에서는 아무런 문제가 없고, 컴파일도 정상적으로 되지만 코드 실행 중에 에러가 발생한다.

 

위와 같은 짧은 코드에서는 함수에 어떤 타입의 인자가 들어가야 잘 작동하고 어떤 타입이 결과로 출력되어야 하는지 바로바로 파악이 가능하지만, 코드가 복잡해질수록 이 과정은 어려워진다.

 

물론 변수명을 잘 짓거나 아래와 같이 독스트링, 주석을 사용해 설명하는 방법도 있지만 독스트링이나 주석 자체에는 타입 설명에 관한 뾰족한 공통 컨벤션이 없기 때문에, 쓰는 입장에서나 읽는 입장에서나 어느 정도 작성 규칙에 관해 노력을 들여야 한다.

def subtract(var_a, var_b):
    """
    Input:
    	var_a (int): Variable A
        var_b (int): Variable B
    Output:
    	subtraction (int): Subtraction of B from A
    """

    subtraction = var_a - var_b
    return subtraction

독스트링이나 주석을 이용해 타입을 명시해 줄 수도 있지만 명확한 포맷이 없을 경우 쓰기도, 읽기도 어려울 수 있다.

 

이번엔 파이썬에서 기본으로 제공하는 타입 힌트(type hint) 기능을 적용해 보자.

Type hinting w/ basic types

def subtract(var_a: int, var_b: int) -> int:
    return var_a - var_b

위 예제는 함수의 인자와 함수의 반환 값의 타입을 명시해 주고 있다. 주석을 사용해 타입을 명시하는 방법보다 보기 쉬우며, 정형화 되어 있다. 이 방식은 클래스의 멤버 함수에도 똑같이 적용할 수 있다.

var_a: int = 5
var_b: str = 'ABCD'
var_c: list = [1, 2, 3]

디폴트 값이 존재하는 함수 인자도 위와 똑같은 방식으로 작성한다.

 

함수의 인자와 반환값 뿐만 아니라, 위와 같이 단순히 변수를 initialize 할 때도 구체적인 타입을 명시해 줄 수 있다.

 

여기서 더 나아가, 더욱 복잡한 형태의 타입 힌트를 적용하고 싶다면 파이썬 내장 모듈인 typing 모듈을 이용할 수 있다. 몇 가지 예시를 통해 간단한 기능들을 알아보자.

TypeVar

from typing import TypeVar, Tuple, List

Integer = TypeVar('Integer', int)

def swap_and_collate(var_a: Integer, var_b: List[str]) -> Tuple[List[str], Integer]:
    return var_b, var_a

위 예제에서는 TypeVar를 이용해 사용자 지정 타입 지정자를 새로 정의하고 있다. 이렇게 만들어진 타입 지정자는 str, int와 같은 디폴트 타입 지정자와 같은 방식으로 사용할 수 있다. 만약 여러 타입을 허용하는 타입 지정자를 표현하고 싶다면, 아래와 같이 TypeVar 이니셜라이즈 시 여러 타입을 동시에 인자로 제공해 주면 된다.

Real = TypeVar('Real', int, float)
T = TypeVar('T', int, float, complex)

List, Tuple

var_list: List[str]
var_tuple: Tuple[str, int, int]

또한 List 뒤에 대괄호로 변수 타입을 지정해 특정한 타입의 리스트를 표현할 수도 있다. Tuple도 마찬가지이다.

Union

from typing import TypeVar, Tuple, List, Union, Callable, Optional

def swap_and_collate(
        var_a: Union[int, float],
        var_b: List[str],
) -> Tuple[List[str], Union[int, float]]:
    return var_b, var_a

인자 타입으로 여러 가지의 타입을 허용하고 싶을 때에는 TypeVar를 이용할 수도 있지만, 위와 같이 Union을 이용할 수도 있다. Union 뒤의 브라켓 안에 허용하고 싶은 타입을 연속으로 나열하면 된다.

 

파이썬 3.10부터는 단순히 '|' 오퍼레이터만을 이용해 표현할 수도 있다.

var_real : int | float

Generic, Callable

from typing import TypeVar, Callable, Optional

Value = TypeVar('Value')
Func = Callable[[Value], Value]


def repetitive_eval(
        func: Func[Value],
        val: Value,
        repeat: Optional[int] = None,
) -> Value:
    if not repeat:
        return val
    for iteration in range(repeat):
        val = func(val)
    return val


def add_one(a: int) -> int:
    return a+1


repetitive_eval(add_one, 1, 5)
>>
6

repetitive_eval(add_one, 1, None)
>>
1

또한 TypeVar를 이용해 제네릭 타입을 구현하거나 Callable를 이용해 특정 타입을 인자로 받아서 특정 타입의 값을 반환하는 콜백 함수를 표현할 수도 있다.

Optional

def repetitive_eval(
        func: Func[Value],
        val: Value,
        repeat: Optional[int] = None,
) -> Value:
    ...

Optional은 인자에 None 값이 전달되는 것을 허용하고 싶을 때 사용하면 된다. 이 경우 위와 같이 None 디폴트 값을 가지는 가변 인자에 적용하고, 함수 내부에서 None을 처리하는 구문을 따로 두는 것이 일반적인 적용 방식이다.

 

이 정도면 대부분의 타입 힌트를 구현하는 데에 문제는 없을 것이다. 더 다양한 기능이 필요하다면, 파이썬 공식 도큐멘테이션을 참고하자.

 

Can type hint prevent RuntimeError?

파이썬 내장 기능과 typing 모듈을 이용한 타입 힌트 테크닉들을 간략하게 살펴 보았다. 그렇다면 이제, 동적이던 부분들을 명확히 해 주었으니 타입 힌팅이 다른 정적 언어들처럼 컴파일 이전 단계에서 에러를 탐지해 줄 수 있는 지 한번 확인해 보자.

def subtract(var_a: int, var_b: int) -> int:
    return var_a - var_b

subtract(3, '5')

>>
TypeError: unsupported operand type(s) for -: 'int' and 'str'

띠용.. 타입 힌트는 정말 바람직한 타입에 대해서 힌트만 줄 뿐이지, 코드 단계에서 문제를 잡아내 주지는 않는다.

 

다만 정형화된 타입 힌트 포맷 덕분에, 정적 분석기(static analyzer)를 따로 이용해서 적절하지 않은 타입이 사용되었을 경우, 경고를 띄워줄 수는 있다.

 

Static analyzer

일반적으로 IDE에서는 언어 별 정적 분석기를 갖추고 있기 때문에, 타입 힌트를 잘 작성해 놓았을 경우 이를 이용해 발생 가능한 문제를 짚어주기도 한다. 다음은 위 코드를 그대로 작성했을 때, Pycharm의 디폴트 인스펙터에서 출력해 주는 경고 메시지를 보여준다.

IDE가 정적 분석을 통해 가능한 문제를 짚어준다.

작성된 타입 힌트와 실제로 대입되는 값이 다를 경우 경고를 띄워 준다! 이 정도의 기능만 있어도 함수 definition을 따라가 내부 코드를 샅샅이 보고 어떤 타입이 들어가 어떤 타입이 출력 되는지 파악해야 할 일이 줄어든다.

 

만약 정말 빡세게, 코드 실행 이전에 타입으로 인한 예상 문제를 확실하게 잡아내고 싶다면, mypy와 같은 정적 분석기 툴을 --strict 옵션과 함께 이용하면 된다. 아래의 예시를 한번 보자.

$ python -m mypy --strict main.py

>>
main.py:5: error: Argument 2 to "subtract" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

꽤 괜찮게 작동한다.

 

하지만 기본적으로 동적 타입 언어로 만들어진 파이썬에서, 타입 힌트와 정적 분석을 모든 곳에 강제함으로써 굳이 동적 타이핑의 장점을 포기할 필요는 없다. 따라서 개발 초기에는 타입 힌트를 사용하지 않으면서 빠르게 코드를 발전시키고, 후반부에는 코드 테스트를 통해 타입으로 인한 문제가 예상되는 곳에만 국소적으로 타입 힌트를 적용하는 편이 더욱 바람직할 것이다.

 


아래는 참고할 만한 사이트들이다.

References

Typing 라이브러리 공식 documentation

Pycharm IDE

반응형