작성 계기
파이썬을 접하게 되었는데, 파이썬은 자바스크립트와 함께 동적 타입 언어입니다.
Dart를 주로 사용했던 저는 변수의 타입이 없는 것이 굉장히 불편하게 느껴졌습니다.
정적 타입 언어: 코드 작성 시 변수의 타입이 정해지는 언어
ex) C, Java
동적 타입 언어: 런타임에 변수의 타입이 정해지는 언어
ex) Python, Javascript
물론 이것은 Python을 사용하는 많은 개발자도 마찬가지였을 것입니다.
그래서 Python에서는 typing이라는 빌트인 모듈을 제공합니다.
이것은 코드 레벨에서 Python 변수에 타입을 명시할 수 있고, IDE는 이를 통해 개발자가 코드를 더 쉽고 안전하게 작성하는 것에 도움을 줍니다.
다음과 같이 변수에 원시 타입을 정해주는 방식만 사용하다가 이는 수박 겉핥기라는 것을 알게 되었습니다.
Python에서 type hint를 더욱 적극적으로 활용하는 법을 정리해 둘 필요성을 느끼게 되었습니다.
#ex
x: int = 1
typing 모듈
파이썬 3.5버전부터 typing 모듈 추가되었습니다.
mypy와 같은 정적 타입 분석기를 통해 python 코드를 실행하기 전에도 타입 오류를 찾아낼 수 있도록 도와줍니다.
하지만, PyCharm에는 JetBrains에서 자체 개발한 타입 분석기가 사용됩니다.
typing 모듈은 PEP 484에 근거해서 개발되었는데, PEP 484의 목표를 간단히 요약하자면 다음과 같습니다.
- 타입의 부재로 인해 발생하는 문제를 해결하기 위해 다양한 방법이 사용되며 혼란이 야기되었습니다.
- 이러한 혼란을 줄이기 위해 파이썬 함수에 annotation을 추가할 수 있는 기능이 개발되었습니다. (PEP 3107)
- PEP 3107는 타입 힌트의 필요성에 대한 근거가 되었고, 타입 힌트에 대한 표준이 필요해졌습니다.
빌트인 타입
파이썬의 내장 타입은 다음처럼 선언해주면 됩니다.
x: int = 1
x: float = 1.0
x: bool = True
x: str = "test"
x: bytes = b"test"
x: list[int] = [1] # Python 3.8 이하에서는 List, Set 등 대문자로 시작합니다.
x: set[int] = {6, 7}
위에 작성된 예시면 충분하다고 생각했는데, 다음 문서를 통해 많은 종류의 새로운 타입에 대해 알 수 있었습니다.
일반적인 정적 타입 언어에서 많이 들어본 타입들이니 문서를 통해 충분히 이해하실 수 있을 것 같습니다.
함수 타입
먼저 파이썬에서 함수에 타입을 지정하는 방법은 다음과 같습니다.
def stringify(num: int) **->** str:
return str(num)
반환값이 없는 void 타입의 함수라면 파이썬에서는 None을 사용합니다.
def show(value: str, excitement: int = 10) -> None:
print(value + "!" * excitement)
Callable
매개변수로 콜백을 넘겨준다면 Callable을 사용합니다.
Callable에는 제네릭으로 매개변수와 반환값을 넘겨줄 수 있습니다.
첫번째 제네릭에는 함수가 받아야 하는 매개변수의 타입을 Iterable 형태로 선언하고,
두번째 제네릭에는 함수의 반환 타입을 선언합니다.
def add(a: int, b: int) -> int:
return a + b
callback: **Callable[[int, int], int]** = add
Awaitable
만약 함수가 비동기 함수라면 Awaitable 타입을 사용합니다.
async def add(a: int, b: int) -> int:
return a + b
callback: **Callable[[int, int], Awaitable[int]]** = add
그 외 유용한 타입
NamedTuple
typing.NamedTuple은 collections.namedtuple의 타입입니다.
namedtuple은 Tuple의 서브 클래스로, tuple과 원소에 이름을 선언할 수 있습니다.
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
# 아래 생성자는 모두 동일한 의미를 가진다.
p = Point(1, 2)
p = Point(1, y=2)
p = Point(x=1, y=2)
DifferentName = namedtuple('Point', ['x', 'y'])
d = DifferentName(1, 2)
d = DifferentName(1, y=2)
d = DifferentName(x=1, y=2)
print(d)
# -> Point(x=1, y=2)
typing.NamedTuple은 collections.namedtuple의 타입이 추가된 버전입니다.
from typing import NamedTuple
# 클래스형
class Point(NamedTuple):
x: int
y: int
# 함수형
Point = NamedTuple('Point', [('x', int), ('y', int)]
# 제네릭도 사용 가능
class Point[T](NamedTuple):
x: T
y: T
NewType
기존과 구분된 타입을 만들 때 사용합니다.
다음과 같이 NewType을 통해 새로운 타입을 생성하면 별칭과 같은 역할을 부여할 수 있습니다.
from typing import NewType
UserId = NewType('UserId', int)
하지만 타입 검사를 실행했을 때, 별칭은 원본 타입과 호환이 가능하지만, NewType은 호환되지 않습니다.
Protocol
프로그래밍 언어에는 duck-typing이라는 특성을 제공하는 언어와 아닌 언어로 나뉩니다.
duck-typing이란, 서로 다른 타입이어도 동일한 속성 및 메서드가 존재하면 호환되는 기능을 말합니다.
예를 들어, 어떠한 객체가 ‘꽥꽥’ 소리를 낸다면 이는 오리이다.라는 원칙에 기반합니다.
class Duck:
def quack(self):
return "꽥꽥!"
class Person:
def quack(self):
return "나는 오리가 아니지만 꽥꽥할 수 있어!"
def make_it_quack(animal):
print(animal.quack())
duck = Duck()
person = Person()
make_it_quack(duck) # "꽥꽥!"
make_it_quack(person) # "나는 오리가 아니지만 꽥꽥할 수 있어!"
typing.Protocol 타입은 이러한 duck-typing 객체에 대한 타입에 힌트를 제공합니다.
위 예시에서 사용된 Duck과 Person은 모두 None을 반환하는 queck 메서드를 가집니다.
typing.Protocol을 활용하면 다음과 같이 정의가 가능합니다.
class Quacker(Protocol):
def quack(self) -> None:
... # …(elipsis)로 구현 생략 가능
저는 typing.Protocol을 보면서 정적 언어의 추상 클래스 및 인터페이스와 유사하다는 생각이 들었습니다.
하지만 의도는 많이 다릅니다.
typing 모듈에서 제공하는 기능들 모두가 그렇듯 typing.Protocol은 런타임에서 에러를 반환하거나 하지 않습니다.
런타임 이전에 수행하는 타입 검사에서 에러를 반환할 뿐입니다.
TypedDict
사실 typing 모듈에 관심을 갖게된 가장 큰 이유는 바로 TypedDict입니다.
Response Body와 같은 JSON 객체에서 key는 str 타입이라는 것을 너무나 잘 알지만 value의 타입을 알기가 어려웠습니다.
typing.TypedDict는 이러한 니즈를 충족시켜주는 기능입니다.
from typing import TypedDict
class UserDict(TypedDict):
id: int
name: str
age: int
class UserDict2(TypedDict):
id: int
name: str
age: NotRequired[int] # age 속성은 필수가 아닙니다.
class UserDict3(TypedDict, total=False): # 모든 속성은 필수가 아닙니다.
id: int
name: str
age: int
user1: UserDict = {'id': 1, 'name': 'Young', 'age': 26}
user2: UserDict2 = {'id': 1, 'name': 'Young'}
typing.TypedDict는 상속을 통해 필드 통합도 가능합니다.
단, TypedDict 끼리만 상속이 가능합니다.
from typing import TypedDict
class A(TypedDict):
a: int
class B(TypedDict):
b: int
class AB(A, B): pass
마무리
typing 모듈에 대해서 간단히 다루어 보았는데, 당연하게도 typing 모듈에는 훨씬 많은 기능이 있습니다.
다른 기능에 대해서도 찬찬히 살펴보면 좋을 것 같습니다.
타입이 없는 파이썬을 공부하면서 아직 많은 시행착오를 겪고 있습니다.
typing 모듈을 통한 타입 힌트는 이러한 어려움을 해소하는데 큰 도움이 되고 있습니다.
이번 글을 통해 typing 모듈 공식 문서 뿐 아니라 PEP와 같은 파이썬 커뮤니티가 발전해나가는 방식과 해당 기능을 개발하거나 제안한 개발자의 의도까지 알 수 있어서 좋았습니다.
다음에는 다른 주제로 또 오지요🍔