개발/파이썬

파이썬 Typing 파헤치기 - 기초편

seonu._.jang 2022. 4. 18. 00:32
반응형

동적 언어에서의 타입 검사

파이썬은 동적 언어로 잘 알려진 언어입니다. 즉, 변수의 타입을 일일이 명시하지 않아도 되고, 특정 변수의 타입이 중간에 바뀌어도 됩니다. 파이썬과 같은 동적 언어는 C, Java, Rust 등의 정적 언어보다 배우기도 쉽고, 좀 더 빠르게 프로그래밍이 가능하다는 장점이 있습니다. 하지만, 잘못된 타입을 사용하여 예상치 못한 에러를 만들어내고, 이로 인해 프로그램이 망가질 수 있다는 단점도 있습니다. 코드 양이 많아지고 복잡해질수록 잘못된 타입으로 인한 에러를 만날 확률이 높아지겠죠.

따라서 최근에는 이런 동적 언어들에도 타입을 명시하고 검사하고자 하는 수요가 늘어나고 있습니다. Javascript계열에서는 Typescript가 많은 인기를 얻고 있으며, 파이썬 같은 경우도 3.5부터 언어 자체에서 Typing (또는 Type Hint)를 지원하고 있습니다. 이렇게 동적 언어에 타입을 명시하고 검사하게 되면 인터프리터 언어의 빠른 개발이라는 장점을 살리면서도 상대적으로 안전한 프로그램을 할 수 있습니다. 이번 포스트에서는 파이썬의 Typing에 대해서 자세히 알아보겠습니다.

파이썬의 Typing 기초

파이썬 3.5 버전 이후부터는 아래와 같이 변수나 함수의 인자, 리턴 값에 타입을 명시할 수 있습니다. 변수나 인자의 경우 변수: <타입>으로 명시할 수 있고, 함수의 응답 값은 경우에는 : 전에 -> <타입>을 붙여주면 됩니다.

a: int = 3

def process_message(msg: str) -> str:
    return msg.strip()

위의 코드를 보면 aint 타입을 가지고 있고 process_message함수는 str타입의 msg를 인자로 받아서 str타입을 리턴하는 것을 한눈에 알 수 있습니다.

이렇게 타입을 명시하면 다음과 같은 이점을 얻을 수 있습니다.

1. 코드의 가독성이 증가합니다.
- 코드에 추가적인 정보 (타입)을 제공함으로써 코드에 대한 이해도가 증가합니다.
2. IDE의 도움을 받으면 코드의 생산성이 증가합니다.
- vscode와 같은 현대 IDE에서는 명시한 타입을 코드를 작성할 때 보여주거나, 자동 완성해주는 기능을 가지고 있습니다. 아래는 파이썬에서 가장 인기 있는 웹 프레임워크 중 하나인 FastAPI를 vscode에서 사용하는 모습입니다. 에디터가 속성과 타입을 보여주고 있습니다.

3. mypypyright와 같은 정적 도구들의 도움으로 사전에 타입 에러를 예방할 수 있습니다.
- 단순히 파이썬에서 타입을 명시해주는 것 만으로는 잘못된 타입을 검사하는 기능을 수행할 수 없습니다. 가독성 향상에만 도움이 될 뿐입니다. 하지만 이를 정적 도구와 함께 사용한다면 잘못 사용된 타입을 사전에 알 수 있습니다.
- mypy는 가장 많이 사용되고 있는 타입 검사도구이며, pyrightmypy와 유사하지만 mypy에 비해 약 5배 빠른 성능을 가지고 있으며, Microsft가 운영하고 있는 라이브러리입니다. 어떤 걸 사용해도 무방하지만, 개인적으로는 pyright를 사용하고 있습니다.
- vscode와 같은 에디터를 사용하면 코드를 작성하면서 잘못된 타입에 대한 피드백을 즉각적으로 받을 수 있습니다. (vscode settings.json에 "python.analysis.typeCheckingMode": "basic" 추가)
-

기초 타입 알아보기

파이썬의 빌트인 타입인 int, str만으로는 우리가 원하는 모든 타입을 명확하게 명시할 수 없습니다. 이런 경우에는 파이썬의 typing모듈의 도움을 받으면 됩니다. 이번 섹션에서는 개인적으로 자주 사용하고 공유하고 싶은 몇 가지 타입들과 그 사용법을 소개하겠습니다.

우선 기본적인 8개의 타입을 소개하고 좀 더 심화된 타입과 심화된 내용은 '심화 편'에서 추가적으로 설명하겠습니다.

1. typing.Union

하나의 함수의 인자에 여러 타입이 사용될 수 있을 때는 typing.Union을 사용하면 됩니다.

from typing import Union

def process_message(msg: Union[str, bytes, None]) -> str:
    ...

위의 코드는 str 또는 bytes 타입 혹은 None값을 인자로 받습니다.

2. typing.Optional

Union[<타입>, None]Optional로 대체할 수 있습니다.

from typing import Optional

def eat_food(food: Optional[str]) -> None:
    ...

위의 코드에서 food의 인자로는 str타입 또는 None값을 받습니다.

참고

파이썬 3.10부터는 Union이나 Optional 타입 대신 |를 사용할 수 있습니다. 즉, Union\[str, bytes, None\]str | bytes | None,Optional\[str\]str | None으로 사용할 수 있습니다.

3. typing.List, typing.Tuple, typing.Dict

리스트(배열) 안에 있는 타입을 나타내 주기 위해서는 typing.List[<타입>]를 사용하면 됩니다. 튜플의 타입을 나타내주기 위해서는 typing.Tuple[<타입>, <타입>]을 사용하면 됩니다. 딕셔너리의 타입은 typing.Dict[<키 타입>, <밸류 타입>]를 사용해주시면 됩니다.

from typing import List, Tuple, Dict

names: List[str]
location: Tuple[int, int, int]
coount_map: Dict[str, int]

참고
파이썬 3.9 이상부터는 리스트, 튜플, 딕셔너리에 대해 typing모듈을 임포트하지 않아도 아래와 같은 타입 명세도 지원합니다.
py

names: list[str]
locatlion: tuple[int, int]
map: dict[str, str]

다만, 라이브러리를 만드신다면, typing모듈을 사용해서 타입 명세를 하셔야 3.9 이전 버전의 파이썬에 호환되는 라이브러리를 만드실 수 있습니다.

4. typing.TypedDict

딕셔너리의 경우 밸류의 타입이 하나로 고정되는 일만 있는 것은 아닙니다. 이럴 때는 TypedDict를 활용할 수 있습니다. TypedDict를 상속받은 클래스를 만드신 다음 아래와 같이 키와 밸류의 타입을 매칭 시켜주면 됩니다. (파이썬 3.8부터 지원)

from typing import TypedDict

class Person(TypedDict):
     name: str
     age: int
     gender: str

def calc_cost(person: Person) -> float:
    ...

또는 아래와 같이 쓸 수 있습니다.

from typing import TypedDict
Person = TypedDict("Person", name=str, age=str, gender=str)
Person = TypedDict("Person", "name": str, "age": int, "gender": str})

참고
TypedDict는 많은 경우에 dataclass로 대체해서 사용할 수 있고, 데이터 객체를 표현하기 위해서는 더 적절할 수 있습니다. (파이썬 3.7 버전부터 지원)

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    gender: str

def calc_cost(person: Person) -> float:
    ...

5. typing.Generator, typing.Iterable, typing.Iterator

어떤 함수가 제너레이터 역할을 하는 경우 리턴 타입으로 Generator[YieldType, SendType, ReturnType]를 명시해주면 됩니다. generator에 대해서 잘 모르시는 분들은 따로 공부해보시길 바랍니다. 참고

def echo_round() -> Generator[int, float, str]:
    sent = yield 0  
    while sent >= 0:
        sent = yield round(sent)
    return 'Done'

위의 echo_round()함수는 float타입을 입력받아서 int타입을 yield하는 제너레이터이며, 음수 값을 받았을 때는 StopIteration 예외와 함께 해당 예외의 값으로 str타입을 리턴합니다.

>>> a = echo_round()
>>> a.send(None)  # Start
0
>>> a.send(3.5)
4
>>> a.send(-3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: Done

제너레이터는 입력(Send) 값과 리턴 값이 있는 경우보다 yield만 하는 경우가 더 많습니다. 이런 경우에는 Generator[YieldType, None, None]으로 타입을 명시하면 됩니다. 또는 아래에서 설명할 typing.Iterable[YieldType] 또는 typing.Iterator[YieldType]을 사용해주시면 됩니다.

import random
from typing import Iterator

def random_generator(val: int) -> Iterator[float]:
    for i in range(val):
        yield i

참고

  • 파이썬 3.9부터는 typing모듈 대신 collections.abc 모듈의 Generator, Iterator, Iterable 등을 사용할 수 있습니다.
  • 제너레이터를 사용할 때 IteratorIterable는 구분할 필요가 없습니다. 다만, 실제 해당 타입은 __next__ 매직 메서드를 구현했느냐 여부로 나눌 수 있습니다. __next__ 매직 메소드를 구분한 객체에 대해서는 Iterator 타입, 그게 아니라면 Iterable타입을 명시하면 됩니다. (두 타입 모두 __iter__ 매직메소드를 사용하는 것을 전제로 하고 있습니다.)

6. typing.Callable

함수를 인자로 가지는 경우에는 Callable[[Arg1Type, Arg2Type], ReturnType] 타입을 활용하면 됩니다.

def on_some_event_happened(callback: Callable[[int, str, str], int]) -> None:
    ...

def do_this(a: int, b: str, c:str) -> int:
    ...

on_some_event_happened(do_this)

Callable 타입에 대해서 좀 더 심화된 내용은 다음 편에서 다루도록 하겠습니다.

참고

Callable한 객체 역시 사용 가능합니다.

7. typing.Type

일반적으로 클래스의 객체는 해당 타입을 그냥 명시하면 됩니다.

class Transaction:
    ...

def process_txn(txn: Transaction):
    ...

하지만 클래스 그 자체를 인자로 받을 경우에는 typing.Type[Class명]을 사용해주셔야 합니다.

class Factory:
    ...

class AFactory(Factory):
    ...

class BFactory(Factory):
    ...

def initiate_factory(factory: Type[Factory]):
    ...

간혹 예외를 인자로 받는 함수들이 있습니다. 이런 경우에도 같은 이유로 Type[예외클래스]를 사용해주시면 됩니다.

def on_exception(exception_class: Type[Exception]):
    ...

8. typing.Any

어떤 타입이든 관계가 없다면 Any를 사용해주시면 됩니다. (가능하면 지양하는 게 좋습니다.)

심화편에서 이어집니다.

기초편에서는 파이썬에서 Typing을 사용함으로써 얻을 수 있는 이점과 기본적인 8개의 타입과 그 사용법에 대해 알아봤습니다. 심화편에서는 조금 더 심화된 타입들과 Typing을 실제 도입하면서 마주할 수 있는 문제와 한계에 대해서 다루어보겠습니다.

오타나 잘못된 내용에 대한 피드백은 언제나 환영입니다. :)

반응형