본문 바로가기

Python 3 정적 타이핑 소개 및 소감(?)

기술적인 이야기/기타 개발 2020. 2. 13.
반응형

파이썬은 동적 타이핑 언어입니다. 쉽게 말해서, 하나의 변수가 타입에 구애받지 않는 데이터를 가리킬 수 있습니다. 그래서 동적 타이핑은 쉽고 간결한 코드를 만들 수 있기도 합니다. 하지만 이런 장점과 함께 단점도 존재합니다.

a = 100
a = "String"

동적 타이핑은 이렇게 단순한 코드를 만들 수 있지만, 변수에 전달되는 값이 과연 원하는 것으로 전달되는 지를 감시할 수 없다는 큰 단점이 있습니다. 예를 들어 특정 함수를 호출할 때 특정 매개변수(parameter)에는 정수가 전달되고 어떤 매개변수에는 문자열이 전달되어야 하는데 이것이 잘못되었다고 할지라도 실제로 실행시키기 전까진 문제가 있는지 없는지 인터프리터가 파악해 주지 않습니다.

def make_double_prev(v):
    return (v * 2) - 1

make_double_prev(10)
make_double_prev("test") # 이 코드가 실행되어야 TypeError 발생

반대로 정적 타이핑의 대표적인 언어인 Swift의 경우 최소한 컴파일 할때 타입이 잘못되었는지를 경고나 에러로 알려줍니다. 그래서 타입이 맞지 않아서 발생하는 런타임 에러를 미연에 잡을 수 있게 해 줍니다.

var name: String = "Conrad"
var nickname = "Seorenn"
name = 100    // 컴파일 시 Error 가 발생

물론 이런 정적타이핑 언어는 코드가 불가피하게 길어진다거나 컴파일 시 오류가 날 확률이 높다는 단점을 가지고 있습니다. 하지만 전달되는 값의 타입을 완벽하게 제어할 수 있다는 큰 장점 때문에 최근에는 이런 정적 타이핑이 다시 흐름새를 타고 있다고 (개인적으로) 느껴지고 있습니다.😏

결국 파이썬에서도 이런 정적 타이핑 기능이 필요하다는 이야기가 적지 않게 들려 왔습니다. 그러다 결국 Python 3 버전에서 정적 타이핑과 비슷한 기능이 소개되기 시작합니다.

 

Type Annotation

Python의 정적 타이핑은 아직 없습니다. 대신 비슷한 기능이 3.0 부터 시작되어서 Function Annotation(3.0+), Type Annotation(3.5+), Variable Annotation(3.6+)로 추가되어 왔습니다. 이것들을 전부 뭉뚱그려서 Type Annotation이라고 대충 부를 수 있으며, 개인적으로는 타입 힌팅이라고 부르겠습니다.

문법 자체는 Swift와 비슷합니다.

variable_name: type

예를 들어 특정 변수는 정수형만 가지게 하려는 것이 목적이라면 아래처럼 코딩할 수 있습니다.

some_value: int = 10

int 라는 키워드가 타입 힌트입니다. 비슷한 방식으로 문자열도 타입 힌트를 줄 수 있습니다.

some_name: str = "Conrad"

함수나 메서드의 매개 변수(parameter) 선언에서도 동일하게 사용할 수 있습니다.

def some_work(name: str):
    pass

사실 이게 타입의 힌트를 알려주는 가장 기본적이면서도 핵심적인 내용입니다.

 

어떻게 체크하나?

그런데 앞서 정적 타이핑이 아니라 Type Annotation 혹은 타입 힌팅이라고 칭한 이유가 있습니다. Python 은 컴파일을 거치는 언어가 아니기 때문에 미연에 타입을 체크하는 단계를 가질 수가 없기 때문입니다.

대신 인터프리터로 실행시키기 이전에 별도의 외부 도구로 타입 힌트들을 체크하는 방법을 도입하고 있습니다.

도구 이름은 mypy 입니다. pip 등으로 설치할 수 있습니다.

pip install mypy

설치가 완료되면 CLI 도구로 사용할 수 있습니다. 사용법도 정말 간단합니다.

mypy filename.py

그냥 소스 파일명이나 경로를 넘겨주면 알아서 체크해줍니다. 예를 들어 아래와 같은 내용을 봅시다.

some_value: int = "string"

이 내용의 파일을 mypy로 체크해보면 아래와 같은 결과가 나옵니다.

somefile.py:1: error: Incompatible types in assignment (expression has type "str", variable has type "int")

이런 식으로 실행시키기 이전에 타입을 제대로 사용하는지 체크할 수 있습니다.

하지만 위의 코드를 python을 이용해 그대로 실행시켜도 아무런 에러가 생기지 않습니다. 파이썬은 아직 정적 타이핑이 없기 때문입니다. 즉 위의 타입 힌트는 mypy로 타입을 체크할 때만 의미가 있고, 실제로 파이썬 인터프리터로 실행시킬 때는 아무런 의미가 없기 때문입니다.

뭐 그래도 문서화에 도움을 주기 때문에 완전히 무의미한 것은 또 아니기도 합니다.

 

복잡한 타입의 힌트

기본적인 타입은 앞서 살펴본 것처럼 정말 단순하게 타입 이름만 적어주면 됩니다. 하지만 리스트(list)나 사전형(dictionary) 같은 복잡한 타입은 그냥은 안 되고 typing 모듈의 힘을 빌려야 정의가 가능합니다.

from typing import List

def some_work_list(values: List):
    pass

List는 파이썬의 대표적인 컬렉션 타입으로 아무 타입의 데이터를 하나 이상 담을 수 있는 자료구조입니다. 위처럼 선언한 경우 이 정의에 맞게 values 매개변수는 타입에 무관한 아이템으로 구성된 리스트를 받으면 문제가 없다고 선언됩니다.

하지만 콜렉션도 명확한 데이터를 가지는 것이 좋습니다. 아래처럼 말이죠.

def some_work_list(values: List[int]):
    pass

이제 values는 정수형(int) 타입의 아이템들로 구성된 리스트만을 인자로 받을 수 있습니다. 앞의 단순한 리스트를 쓰는 것에 비해 의도와 한계를 명확하게 알려줄 수 있지요.

사전형(Dictionary) 타입의 경우도 비슷하게 선언할 수 있습니다.

from typing improt Dict

def some_work_dict(values: Dict[str, int]):
    pass

위 예제는 values 매개변수는 문자열 타입의 키와 정수형 타입의 값을 가지는 사전형 타입을 가져야 한다는 힌트를 정의하는 코드입니다.

이 외에도 Tuple 같은 타입 힌트 등 다양한 타입 힌트를 typing 모듈에서 찾을 수 있습니다.

 

좀 더 복잡한 타입?

리스트나 튜플, 사전형의 경우 각 아이템의 타입이 다채로울 수 있습니다. 만약 키는 문자열이지만 값은 아무 타입을 가질 수 있는 사전형 타입 힌트를 정의하려 한다면 아래처럼 할 수 있습니다.

from typing import Dict, Any

def some_work_dict(values: Dict[str, Any]):
    pass

Any라는 특수한 타입 힌트가 쓰였습니다. 아무 타입이라는 의미로 추측이 가능합니다. Swift에서 쓰이는 any와 의미가 비슷하기도 하네요.

이 방법 외에도, 만약 Any를 쓰기가 싫은 대신 명확한 몇 가지 타입만을 가지면 된다는 힌트를 주고 싶다면 아래처럼 Union을 사용할 수도 있습니다.

from typing import Dict, Union, List

def some_work_dict(values: Dict[str, Union[str, int, List[str]]]):
    pass

여기서 매개변수 values는 문자열 키와 함께 문자열, 정수, 문자열 리스트를 값으로 선언할 수 있는 사전형 타입의 힌트를 정의하고 있습니다. 뭔가 복잡해 보이지만 이런 식으로 콜렉션 안에서도 컬렉션 타입을 쓸 수 있다는 것을 같이 볼 수 있는 좋은 예제 같습니다.

 

Optional?

앞서 본 예제들은 NULL과 비슷한 None 값을 가질 수가 없다는 특징이 있는데 이 값을 가지려면 어떻게 해야할까요?

우선 앞서 소개한 Union을 이용하는 방법이 있습니다.

some_value: Union[int, None] = None

위의 코드에서 some_value 변수는 정수형 혹은 None 값을 가질 수 있습니다.

하지만 이를 좀 더 편하게 할 수 있는 것이 있습니다. 바로 Optional 입니다.

from typing import Optional
some_value: Optional[int] = None

Optional의 의미는 '이 타입은 혹은 None'이라는 의미를 가지고 있습니다. 즉 위 코드에서 some_valueint 타입뿐만 아니라 None도 값으로 가질 수 있다고 힌트를 명시하고 있습니다. 아무래도 이 Optional을 쓰는 방법이 Union을 사용하는 것보다는 더 가독성이 좋은 것 같습니다.

기타

글이 너무 길어질 것 같아서 참고할 수 있을만한 내용을 간단히 언급하겠습니다.

  • 만약 새로운 타입을 정의하고 싶다면 NewType 에 대해 찾아봅시다.
  • 연관 타입을 정의하고 싶다면 TypeVar 를 찾아봅시다.
  • 함수 타입은 Callable 을 찾아봅시다.

이 외에도 뭔가 너무 많아서 빠트린 게 많을 것 같습니다.

 

결론

안타깝게도 파이썬은 아직 정적 타이핑이 제공된다고 보기에는 힘듭니다.

PEP-484 "No type checking happens at runtime" (실행시킬 때는 타입 체크를 하지 않는다)

사람마다 원하는 바는 다를 것이기에 결론 또한 다를 수 있습니다.

저는 적어도 현재의 타입 힌트만 주는 기능은 정적 타이핑이라고 생각하지 않습니다. 최소한 인터프리터가 시작 전 강제로 타입을 체크하도록 하는 것이 필요하겠지요. 하지만 그렇게 해버리면 퍼포먼스 문제도 생길지도 모르니 당연히 생각처럼 쉽게 되는 일 또한 아니겠지요. 그래서 뭐가 맞다고 이야기 하기에도 개인적으로 미숙할 뿐입니다.

어쨌거나 mypy라는 외부 도구를 이용해서 정적 타이핑을 흉내 낼 수 있다는 점 만으로도 파이썬 프로그래밍의 세계가 좀 더 견고해지리라는 것은 확실합니다. 거기다 몇몇 IDE 에디터들은 이미 mypy로 연동을 하고 있는 수준이기 때문에 편집 단계에서 문제를 미연에 해결하는 것 또한 가능한 수준입니다.

뭐... 쓸 만하다는 이야기입니다. :D

728x90
반응형

댓글