파이썬은 자유로운 영혼이에요

Python의 장점 중 하나를 꼽으라면 동적 타입 언어(dynamic typing language)라는 점입니다. 실행하는 시점에서 변수의 타입을 고려하기 때문에 타입을 신경쓰지 않고 코딩이 가능하죠. 아래 코드는 파이썬의 대표적인 특징을 잘 나타내고 있습니다.

def add(a, b):
    return a + b

print(add(1, 3))  # 4
print(add('item', '4'))  # 'item4'
print(add([1, 2], [3, 4]))  # [1, 2, 3, 4]

하지만 안 되는건 안 된다고

하지만 아무리 파이썬이 강력해도 모든걸 알아서 해주지는 않습니다. 가령 다음과 같은 코드는 에러를 내겠죠.

add('item', 4)  # TypeError!

동적 타입 언어는 실행 시점에서 타입을 평가하는 것이지, 타입이 없는 언어는 아닙니다. 타입이 맞지 않게 들어가면 당연히 에러가 발생합니다.

if문으로 검사하면 될까요?

혹자는 이렇게 생각하실 수도 있습니다. if문으로 조건을 걸어서 확인하면 된다구요.

가령 이런 식으로 말입니다.

def add(a, b):
    if type(a) == type(b):
        return a + b
    raise TypeError('incorrect type')

하지만 에러가 난다는 점에는 변화가 없습니다. 에러가 나기 전에 미리 알 수 있는 방법은 없을까요?

짠! 나는 정적 타입 검사의 요정이야!

이런 문제를 손쉽게 해결하는 방법은 바로 에디터에서 미리 알려주는 것 입니다. 에디터가 지원하지 않는다면 하다못해 commit이나 push전에 알 수 있으면 좋겠죠. 현대적인 IDE들은 대부분 자동완성에 타입 지정을 인식합니다.

한번 방법을 알아볼까요?

1. 내 의도에 맞는 타입을 지정한다.

Python 3부터는 Type Annotation이라는 문법이 추가되었습니다. 3.6부터는 일반 변수에도 사용할 수 있죠.

def add(a: int, b: int) -> int:  # int형 변수 a와 b를 입력받아서 int형 값을 반환
    return a + b

참 쉽죠?

2. 에디터에서 지켜본다

저는 JetBrains 계열의 에디터를 주로 사용합니다. 1번을 적용해서 에디터상에서 보면 다음 부분은 이렇게 됩니다.

IntellJ + Python Plugin

끝입니다. 에디터가 고치라는 곳을 확인해보면 되요. 참 쉽죠?

mypy

하지만 JetBrains 제품은 비쌉니다. 모두가 이걸 사용하리란 보장도 없고, 에디터상에서만 확인 가능한 경우 모두가 같이 확인하기 곤란합니다. 이럴때 쓰기 좋은 것이 mypy입니다.

$ pip install mypy
$ mypy add.py
add.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int"
add.py:6: error: Argument 2 to "add" has incompatible type "str"; expected "int"
add.py:7: error: Argument 1 to "add" has incompatible type List[int]; expected "int"
add.py:7: error: Argument 2 to "add" has incompatible type List[int]; expected "int"

하지만 제 자료형은 복잡한걸요?

하지만 현실의 자료형이 intstr 같은 것만 있는 것은 아닙니다. 당장에 생각해볼 수 있는 listdict, tuple 등의 자료형이 존재하고, 이들은 자료를 내장하는 컨테이너라서 보다 복잡한 표현식이 필요할 것 같습니다. 이런 문제 해결을 위해 typing 모듈이 존재합니다.

from typing import Dict, List, Tuple

class C:

    def __init__(self, value: str) -> None:  # __init__은 반드시 None을 반환합니다.
        self.value = value


a: C = C('item4')
members: List[C] = [C('item4'), C('meta'), C('marnitto')]
data: Dict[str, List[Tuple[C, int]]] = {
    '사람': [(C('철수'), 100), (C('영희'), 80)],
    '동물': [(C('바둑이'), 0)],
}


def print_list(list_value: List[C]):
    for x in list_value:
        print(x.value)


def get_score_max(
    category: str,
    data: Dict[str, List[Tuple[C, int]]]) -> Tuple[C, int]:
    return max(data[category], key=lambda x: x[1])


print_list(members)

print(get_score_max('사람', data))

mypy를 통해 검사하면 보다 확실하게 확인이 가능합니다.

근데 인간적으로 너무 길지 않습니까?

타입 선언이 너무 길고 중복적으로 등장하는 것이 불편하실 수 있다고 생각합니다. 다행히도 mypy등의 정적타입 검사기들은 alias를 지원합니다.

위 코드는 이런 식으로 고칠 수 있죠.

from typing import Dict, List, Tuple

class C:

    def __init__(self, value: str) -> None:  # __init__은 반드시 None을 반환합니다.
        self.value = value

MemberList = List[C]  # alias를 만든다
Record = Tuple[C, int]
DataDict = Dict[str, List[Record]]  # alias 속에 alias를 넣을 수도 있다.
a: C = C('item4')
members: MemberList = [C('item4'), C('meta'), C('marnitto')]
data: DataDict = {
    '사람': [(C('철수'), 100), (C('영희'), 80)],
    '동물': [(C('바둑이'), 0)],
}


def print_list(list_value: MemberList):
    for x in list_value:
        print(x.value)


def get_score_max(category: str, data: DataDict) -> Record:
    return max(data[category], key=lambda x: x[1])


print_list(members)

print(get_score_max('사람', data))

훨씬 낫죠?

더 복잡한 자료를 다루는 예시

namedtuple

Python의 내장 자료형중 tuple의 확장형인 collections.namedtuple은 자료를 다룰때 매우 용이합니다. namedtuple에 정적 타이핑을 하려면 다음과 같이 합니다.

from typing import List, NamedTuple


class Record(NamedTuple):

    name: str
    score: int
    addresses: List[str]


item4 = Record('김진수', 100, ['경기도 부천'])
troll = Record('김트롤', 0, '청와대')  # 마지막 인자가 List[str]이 아니므로 에러

TypeVar를 통한 Generic

위에서 만지작 거렸던 add를 TypeVar를 사용하여 좀 더 자유분방하게 쓸 수 있게 개조해보겠습니다.

from typing import List, TypeVar

T = TypeVar('T', int, str, List[int])  # T는 int, str, List[int]일 수 있습니다.

def add(a: T, b: T) -> T:
    return a + b


print(add(1, 3))  # [int, int] -> int
print(add('item', '4'))  # [str, str] -> str
print(add([1, 2], [3, 4]))  # [List[int], List[int]] -> List[int]
print(add('item', 4))  # Error!

실제로 mypy를 돌려보면 다음과 같이 됩니다.

$ mypy add.py
add.py:12: error: Type argument 1 of "add" has incompatible value "object"

TypeVar는 이런 식으로도 사용이 가능합니다.

from typing import List, TypeVar

T = TypeVar('T')


def get_last(data: List[T]) -> T:
    return data[-1]


print(get_last([1, 2, 3]))  # List[int] -> int
print(get_last(['item4', 'is', 'developer']))  # List[str] -> str

Union

여러 종류의 타입이 섞여 와도 상관 없는 경우가 가끔 있습니다. 그런 경우엔 Union을 사용합니다.

from typing import Union


def add10f(a: Union[int, float]) -> float:
    return 10.0 + a  # a에 int가 오건 float이 오건 float을 반환


print(add10f(1))
print(add10f(2.3))

자주 나오는 질문

왜 동적 타입 언어에 굳이 정적 타이핑을 해야하나요?

실수를 줄이기 위해서입니다. 실행시키기 전에 파악할 수 있는 에러는 미리 잡는 것이 효율적이겠죠?

속도가 빨라지나요?

미미한 변화가 있을지는 모르지만 별 차이는 없습니다. (더 빠를 수 있을지도 모르지만 더 느려지진 않는다는 의미)

아무 타입이나 다 받고 싶을땐 어떻게 하죠?

아예 타입을 적지 않거나, typing.Any를 사용하시면 됩니다.

저는 Python 2 사용자인데 사용할 수 없나요?

있습니다! PEP-484에 따라 다음과 같은 컨벤션을 따르면 됩니다.

class A:

    def __init__(self, value):
        # typing: str -> None
        self.value = value

a = A('item4')  # typing: A

보다 많은 정보를 얻고 싶어요!

다음 링크들을 참조하세요.