[Python] Type hint/annotation (with typing module)

정준환·2022년 12월 25일
0

C나 Java와 달리, Python은 type을 지정 안해줘도 잘 굴러가는 언어다. 쉽고 간편하게 코드를 작성할 수 있는 장점도 있지만, 프로젝트 크기가 커질수록 관리가 힘들어진다. Javascript 진영에서 Typescript가 대세가 된 것 처럼, Python에서도 type을 지정하는 요구가 커지고있다.

(본 글은 python 3.8 버전을 기준으로 작성했습니다.)

mypy 설치하기


mypy를 이용하면 hint에 맞는 제대로 된 변수가 들어갔는지 체크를 해준다.

$ pip install mypy
$ conda install -c conda-forge mypy

mypy를 설치해주고, 아래와 같이 파일을 실행하면 된다.

$ python main.py <- 이거 대신
$ mypy main.py <- 이렇게 하면 된다. 

그럼 이렇게 오류가 있는지 없는지 찾아준다!
이제 본격적으로 Python에서 Type 지정을 어떻게 하는지 알아보자.

기본 1


변수에 hint 달기

var1 = 1
var1: int = 1

var2 = "정준환"
var2: str = "정준환"

함수에 hint 달기

def foo(x):
    return x

def foo(x: str) -> str:
    return x

너무 간단해서 보기만 해도 이해할 수 있다. 조금 더 진화해보자. 이런 진화를 위해서는 typing 이라는 친구의 도움이 필요하다. Python을 설치할 때 같이 설치되므로 따로 설치할 필요는 없다.

Union: 여러가지 type이 가능하도록 하기

def plus(x, y):
    return x + y
  • 위의 plus라는 함수에서 x, y는 int와 float 둘 다 가능하다. 그럼 type을 어떻게 주지!
  • 아래와 같은 방법을 사용한다.
  • Union은 여러가지 type이 가능할 때, 이 여러가지 type이 모두 가능해요~ 라고 알려주는 역할이다.
from typing import Union

def plus(x: Union[int, float], y: Union[int, float]) -> Union[int, float]:
    return x + y

더 읽어보기 (안 읽어도 된다는 뜻)

  • PEP 3141에서 숫자에 대한 규약을 했다.
  • 위의 예시로 Union[int, float]를 했는데, 위 링크에서는 float가 되면 int가 무조건 되니까 int는 굳이 언급하지 않는 방향이 효율적이라고 말한다.
  • 근데 또 mypy에서는 int와 float를 구분한다. 즉, float 자리에 int가 들어가면 오류가 발생한다.
  • 아래 코드와 같은 방법도 가능해야 할 것 같은데 mypy에서는 오류가 난다. 검색해보니까 mypy github에 이슈가 잔뜩 달려있다. 해결 방법도 간략하게 소개되어 있으니까 더 궁금하면 찾아보자.
from numbers import Number, Real

def any_func(n: Number)
def any_func(n: Real)

Optional: None type은 따로 명시해주기

  • 위의 Union을 활용하면 Union[int, None] 등 과 같은 방법으로 None이 가능하다고 표현할 수 있다.
  • 그런데 typing 모듈은 다른 방법도 제공한다.
  • Optional 이라는 놈인데, 말 그대로 해당 인자가 option이라는 것을 알려주는 것이다.
from typing import Union, Optional

def foo_1(x: Union[str, None]) -> str:
	return "정준환"

def foo_2(x: Optional[str]) -> str:
	return "정준환"
  • 위의 foo_1, foo_2는 동일하다.
  • foo_2가 조금 더 직관적이라 권장되는 방법인 것 같다.
  • 그런데 여기서 문제가 생긴다.
  • Optional은 단 하나의 인자만 허용한다…
  • 아래의 형식이 불가능하다는 소리다.
from typing import Optional

Optional[int, float]

그럼 어떻게 해요?

  • 아래를 보자. 이런 방식으로 해결한다고 한다.
from typing import Union, Optional

Optional[Union[int, float]]

편한 방법

  • 사실 python 3.10 버전에 들어오면서 아래와 같은 형식이 가능해졌다.
  • 근데 이전 버전과의 호환성을 위해 당분간은 그냥 typing을 써야할 것 같다.
def foo_3(x: int | float) # 3.10 이상

기본 2


List 안에 있는 type까지 알려주기

아래는 list에 있는 숫자를 다 더해주는 함수다.

def plus(a_list: list) -> int:
    summ = 0
    for n in a_list:
        summ += n
    return summ
  • 근데 살짝 문제가 있다.
  • 아래 코드를 한번 살펴보자.
str_list = ["안녕", 1, 2, 3]
plus(a_list=str_list)
  • 이렇게 해도 str_list는 list긴 하니까 type 관점에서 보면 문제가 없다.
  • 이를 해결하려면 list 안에 있는 원소들의 type까지 알려줄 수 있으면 좋을 것 같다.
from typing import List

def plus(a_list: List[int]) -> int:
    summ = 0
    for n in a_list:
        summ += n
    return summ
  • 위와 같은 방법을 사용할 수 있다.
  • Union을 활용하면 아래와 같은 예시도 가능하다.
from typing import List, Union

def plus(a_list: List[int | float]) -> int | float: # 3.10 이상
def plus(a_list: List[Union[int, float]]) -> Union[int, float]: # 이전

Tuple 인자 알려주기

tuple은 list와 비슷하긴 한데, 한번 만들어지면 못바꿔서 list와는 상황이 조금 다르다. 모든 요소에 대한 type을 다 적어줘야한다.

from typing import Tuple

x: Tuple[int, float, str] = (1, 1.2, "정준환")
y: Tuple[int, int, int] = (1, 2, 3)

이렇게 쓴다. 근데 좀 귀찮지 않나?

  • 그래서 같은 타입일 때 생략하고 싶으면 “...”을 이용하는 방법도 있다.
  • 자연스럽게, 제일 앞에 타입을 써주고, 두번째 자리에 …을 써주면 된다.
from typing import Tuple

a: Tuple[int, ...] = (1, 2, 3, 4) # O
x: Tuple[int, int, ...] = (1, 2, 3, 4) # X
y: Tuple[..., int] = (1, 2, 3, 4) # X
z: Tuple[int, ..., int] = (1, 2, 3, 4) # X

Dict, Set 에서 key, value type 알려주기

이정도 왔으면 대충 설명해도 잘 알아들을 수 있을 것 같다.

from typing import Dict, Set

x: Dict[str, float] = {"field": 2.0}
x: Set[int] = {6, 7}

편한 방법

  • 마찬가지로 얘도 typing module 사용 안하는 방법이 있다.
  • python 3.9 부터 그냥 list[int], dict[str, float] 이런게 가능하다. 대소문자에 유의해서 보자!
  • 근데 마찬가지로 호환성 이슈로 인해 일단은 그냥 불편하게 사용하는 중이다.

심화


Callable: 함수를 넘겨주기

def add(x: float, y: float) -> float:
	return x + y

def mul(x: float, y: float) -> float:
	return x * y

위와 같은 간단한 두가지 함수가 있다고 하자.

def func_n_times(func, x: float, y: float, n: int) -> float:
	"""
	주어진 function을 n번 적용해서 더하는 함수
	"""
	return_value = 0
	for i in range(n):
		return_value += func(x, y)
	return return_value

억지로 예시를 하나 만들어봤다. 이런 경우에 func에는 어떤 방식으로 hint를 줄 수 있을까?
아래와 같은 방식을 이용한다. 별로 어렵지 않다.

from typing import Callable

def func_n_times(func: Callable[[float, float], float], x: float,,,)
# Callable[[함수의 input type], return type]

Tuple의 경우랑 비슷하게 return 만 보고싶으면 ...을 활용할 수 있다. 아래처럼 하면 된다.

from typing import Callable
Callable[..., float]

Iterable: 조금 더 일반화 해보기

from typing import List

def plus(a_list: List[int]) -> int:
    summ = 0
    for n in a_list:
        summ += n
    return summ

위에서 썼던 plus 함수를 다시 가져왔다.

  • 이 함수는 list에서만 작동할까? 아니다.
  • 이 함수는 a_list라는 놈이 set, tuple 무엇이든 간에 아무튼 for loop을 돌릴 수 있으면 작동할 수 있다.
  • 그러면 그냥 List[int] 라고 주는 것은 오히려 잘못된 type hint일 수 있다.
  • 이러한 경우를 어떻게 해결할까? 비슷한 친구들을 한데 묶어볼 순 없을까?
from typing import Iterable

def plus(a_list: Iterable[int]) -> int:
    summ = 0
    for n in a_list:
        summ += n
    return summ

이렇게 Iterable 객체는 Iterable 이라고 따로 지정해줄수도 있다!

더 알아보기

  • Iterable, Sequence, Iterator에 대해서 알아보면 좋을 것 같다.

TypeVar: Union의 진화형

아래 예시코드를 살펴보자.

from typing import Union

def any_f(x: Union[str, int]) -> Union[str, int]:
	return x

str, int 중 아무거나 들어오면 str, int 중 아무거나 내뱉는다. 어라? 조금 이상한데?

보통 우리는 str이 들어오면 str을, int가 들어오면 int가 return되기를 희망한다. 앞서 배운 Union으로는 이 한계를 해결할 수 없다. 이것을 해결한 것이 바로 TypeVar다. 아래 코드를 살펴보자.

from typing import TypeVar

T = TypeVar("T")

def any_f(x: T) -> T:
	return x
  • 이런식으로 지정해주면 x에 들어간 T라는 type이 그대로 나온다.
  • 여기에 위 처럼 str, int와 같은 타입에 제한을 두고싶다면 아래처럼 쓸 수 있다.
  • 이 때, bound 안에 들어간 type을 상속한 type들도 가능하다.
from typing import TypeVar, Union

T = TypeVar("T", bound=Union[str, int])

A = TypeVar("A")
B = TypeVar("B")
T_abc = TypeVar("T_abc")
  • T = TypeVar("T") 에서 앞 뒤의 이름(T와 TypeVar 안에 “T”)을 같게 해주는게 국룰이다. 맞춰주도록 하자!
  • 정리해보면 Union은 진짜 아무거나, TypeVar는 아무거나는 맞는데, 들어온 것과 나간것이 동일!

이정도 하면 Python에서 type hint를 주는데 큰 문제가 없을 것 같다. 깔끔하게 코드를 짜보자!

도움

https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#
https://peps.python.org/pep-0484/#the-numeric-tower
https://stackoverflow.com/questions/72157296/python-iterable-vs-sequence

profile
정준환

0개의 댓글