Python/ETC / / 2023. 5. 6. 15:25

[번역] 파이썬에서의 타입 체킹, mypy를 중심으로

 

 

Python Type Checking

This article looks at what type hints are and how they can benefit you. We'll also dive into how you can use Python's type system for type checking.

testdriven.io

타입 체킹이란 무엇일까? 왜 필요할까? 정적 타입하고 런타입 타입체킹하고 차이는 무엇일까?

 

파이썬은 강력한 동적 프로그래밍 언어이다. 동적으로 코드를 타이핑하면, 타이핑한 것은 동적으로 추론되며 자바같은 정적 타입 프로그래밍 언어와 달리, 변수 값을 직접적으로 타입 정의 없이 세팅할 수 있다.

 

name = "Michael"
String name = "Michael"

강하면서 동적이라는 말은 타입들이 런타임 시에 추론되지만, 타입들 끼리 섞을 수 없다는 말이다. 예를 들어 a = 1 + "0"은 파이썬에서 에러를 일으킬 것이다. 반대로, 자바스크립트는 약하면서 동적이란 말은 런타임 시에 추론되면서 타입들 끼리 섞을 수 있다는 말이다. 예를 들어, a = 1 + "0"을 하면 a는 10이 될 것이다. 

동적 타이핑이 유연성을 가지는 반면 - 언제나 희망 사항은 아니지만, 나중에 가서 동적 언어에 정적 타입 추론을 적용할 노력을 할 필요가 생기기도 한다. 이 글에서는, 어던 타입 힌트가 어떻게 당신에게 도움을 줄지 볼 것이다. 또한 mypy를 통해 정적 타입 체킹을 파이썬의 타입 시스템을 사용하는지 파고볼 것이며, 그리고 pydantic, marshmallow 그리고 typeguard를 통해 런타임 타입 체킹을 사용할 수 있는지 알아볼 것이다.

 

툴 - Tools

몇몇 툴들이 타입 힌트를 이용하여 정적과 런타임 타입 체킹을 할 수 있게 도와준다.

Static typing

  1. mypy
  2. pyre
  3. Pyright
  4. pytype
  5. pyanalyze

Runtime type checking / data validation

  1. marshmallow
  2. pydantic
  3. typeguard
  4. typical
  5. pytypes

Project specific

  1. pydantic-django
  2. django-stubs
  3. typeddjango
  4. flask-pydantic
  5. flask-marshmallow
  6. fastapi (pydantic is built in -- yay!)

Awesome Python Typing을 확인해보자

 

타입 힌트 - Type Hints

파이썬 3.5에서부터 타입 힌트가 추가되었다. 이러한 타입 힌트는 개발자로 하여금 파이썬 코드에서 변수, 함수 파라미터, 그리고 함수의 리턴값의 기대되는 타입들을 명시해준다. 물론 이러한 타입들은 파이썬 인터프리터로 하여금 강제되는 것은 아니지만 - 다시금 말하지만, 파이썬은 동적 타입 언어이다 - 몇몇 이점을 제시해준다. 먼저 타입 힌트를 통한 강력한 것은 코드를 통해 하려는 것과 어떻게 사용하는지에 대한 의도를 더 좋게 표현할 수 있다는 것이다. 더 좋은 이해는 적은 버그의 결과로 나온다.

 

예를 들어, 나날의 기온 평균을 계산하는 함수를 작성했다고 하자.

def daily_average(temperatures):
    return sum(temperatures) / len(temperatures)

기온 리스트를 제대로 넣어주는 한, 함수는 의도한대로 작동하며, 기대된 결과도 리턴해준다.

average_temperature = daily_average([22.8, 19.6, 25.9])
print(average_temperature)  # => 22.76666666666667

만약 키값이 측정 값의 타임스탬프이고 밸류는 측정 값인 딕셔너리를 넣으면 어떻게 될까?

average_temperature = daily_average({1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9})
print(average_temperature)  # => 1599125872.6666667

필연적으로 이 함수는 "키값의 합 / 키값의 수"를 리턴하며 이는 아주 잘못된 결과이다. 아무래도 함수가 에러를 발생시키지 않으니, 특히 유저에게 제공되는 이 온도는 곧 발견되지 못한다. 이러한 혼동을 피하기 위해 파라미터랑 리턴 값에 타입 힌트를 명시할 수 있다.

def daily_average(temperatures: list[float]) -> float:
    return sum(temperatures) / len(temperatures)

이러한 함수 정의는 우리에게 

  1. temperatures 소수 리스트로 이루어져 있어야 한다 : temperatures: list[float]
  2. 함수는 소수를 리턴해야 한다 : -> float
print(daily_average.__annotations__)
# {'temperatures': list[float], 'return': <class 'float'>}

타입 힌트는 정적 타입 체킹 툴을 가능하게 해준다. 코드 에디터나 IDE는 이를 잘 사용하고 있으며, 타입 힌트에 따라 특정 함수나 메소드를 사용할 때 경고해주며, 강력한 자동완성을 제공해준다. 그치만, 타입 힌트는 말 그대로 "힌트"이다. 다른 말로, 이는 정적 타입 언어에서처럼 타입 정의가 강제가 아니다. 이것이 의미하는 것은, 비록 꽤나 유연하지만 의도를 더욱 명확하게 표현해서 코드 퀄리티를 향상시켜준다는 것이다.  이를 통해 아주 많은 툴에게서 이득을 더 얻을 수 있따.

 

Type Annotations vs Type Hints

타입 어노테이션은 그저 함수 인풋, 아웃풋 그리고 변수들을 표시해주는 문법에 불과하다.

def sum_xy(x: 'an integer', y: 'another integer') -> int:
    return x + y


print(sum_xy.__annotations__)
# {'x': 'an integer', 'y': 'another integer', 'return': <class 'int'}

타입 힌트는 더 유용하게 쓸 수 있게 annotation위에 속해 있다. 힌트와 어노테이션은 가끔씩 상호호환적으로 사용되지만, 둘은 다르다.

 

Python의 타이핑 모듈

가끔씩 이러한 코드를 보고서 의문을 품은 적이 있을 것이다.

from typing import List


def daily_average(temperatures: List[float]) -> float:
    return sum(temperatures) / len(temperatures)

리턴 타입을 정의하기 위해 원래 있던 float를 사용했지만, List는 typing모듈에서 임포트 되었다. 파이썬 3.9 이전에서는 파이썬 인터프리터는 인자에 대한 타입 힌팅을 위해 빌트인 타입들을 지원하지 않았다. 예를 들어 list를 타입힌트처럼 다음과 같이 사용할 수 있다.

def daily_average(temperatures: list) -> float:
    return sum(temperatures) / len(temperatures)

하지만 타이핑 모듈 없이 기대되는 리스트의 원소(list[float]) 타입들을 정의하는 것은 불가능했다. 이는 딕셔너리나 다른 연속체 그리고 복잡한 타입들에서도 똑같이 적용된다.

from typing import Tuple, Dict


def generate_map(points: Tuple[float, float]) -> Dict[str, int]:
    return map(points)

이와 다르게 타이핑 모듈은 새로운 타입(new types), type aliases, type Any 그리고 여러 다른 타입들을 정의할 수 있게해준다. 예를들어 다양한 타입들을 포함시키고 싶다면 Union을 사용할 수 있다.

from typing import Union


def sum_ab(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:
    return a + b

파이썬 3.9부터는 다음과 같이 사용할 수 있다.

def sort_names(names: list[str]) -> list[str]:
    return sorted(names)

 

mypy를 통한 정적 타입 체킹 

mypy는 컴파일 타임에서 타입 체킹을 할 수 있는 툴이다.

pip install mypy
python -m mypy my_module.py
def daily_average(temperatures):
    return sum(temperatures) / len(temperatures)


average_temperature = daily_average(
    {1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9}
)

위의 예시를 mypy를 통해 돌리면 아무 에러도 안뜬다. 왜냐하면 함수가 어떠한 타입 힌트도 사용하지 않았기 때문이다.

Success: no issues found in 1 source file
def daily_average(temperatures: list[float]) -> float:
    return sum(temperatures) / len(temperatures)


average_temperature = daily_average(
    {1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9}
)

위의 코드를 python -m mypy my_module.py를 통해 돌리면 다음과 같은 에러를 확인할 수 있다.

my_module.py:6: error: Argument 1 to "daily_average" has incompatible
type "Dict[int, float]"; expected "List[float]"  [arg-type]

Found 1 error in 1 file (checked 1 source file)

mypy는 함수가 잘못 호출되었다는 것을 인식하고 있다. mypy는 파일 이름, 라인 넘버 그리고 에러에 대한 설명을 리포트한다. mypy와 함께 타입 힌트를 사용하면 함수, 메서드 그리고 클래스의 잘못된 사용을 줄일 수 잇다. 이는 빠른 피드백 루프를 형성시킬 수 있다. 모든 테스트를 돌릴 필요가 없어지며, 혹은 전체 어플리케이션을 배포할 때도 말이다. 당신은 즉시 에러를 통보받을 수 있다. 또한 mypy를 CI(Continuous Integration)에 포함시켜서 당신의 코드가 머지되고 배포되기 전에 타이핑을 체크하는 것도 좋은 아이디어이다. 이에 대해서는 Python Code Quality 글을 읽어보자.

 

비록 코드 퀄리티 측면에서는 큰 향상일 수 있지만, 정적 타입 체킹은 당신의 프로그램이 돌고 있을 때 런타임 상에서 강제하지 않는다.

mypy는 파이썬 정식 라이브러리와 파이썬의 빌트인들, 그리고 서드 파티 패키지을 위한 외부 타입 어노테이션을 포함한 typeshed와 같이 딸려온다. 기본적으로 mypy는 파이썬 프로그램을 런타임 오버헤드 없이 체크한다. 비록 타입을 체크하지만, 덕 타이핑이 일어날 수 있따. 따라서 CPython의 확장팩으로 사용할 수 없다.

 

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유