Python 3.5부터 도입된 Python의 typing 모듈은 정적 타입 체커와 린터가 오류를 정확하게 예측할 수 있도록 타입 힌트를 제공하는 시도를 합니다.
런타임 중에 Python이 객체의 타입을 결정해야 하는 경우, 개발자는 코드 내에서 무슨 일이 일어나고 있는지 알아내기가 어려울 때가 있습니다.
외부 타입 체커인 PyCharm IDE와 같은 도구도 최상의 결과를 내지 않습니다. StackOverflow의 이 답변에 따르면 평균적으로 오류를 올바르게 예측하는 경우는 50%에 불과합니다.
Python은 이 문제를 완화하기 위해 외부 타입 체커가 어떤 오류도 식별할 수 있도록 타입 힌팅(타입 주석)을 도입하는 것이 알려져 있습니다. 이는 프로그래머가 컴파일 시간에 사용되는 객체의 타입을 힌트로 제공하여 타입 체커가 정확하게 작동하도록 하는 좋은 방법입니다.
이로써 Python 코드는 다른 독자들에게 훨씬 더 가독성이 있고 견고해집니다!
참고: 이것은 컴파일 시간에 실제로 타입을 체크하지 않습니다. 힌트로 주어진 타입과 실제로 반환된 객체의 타입이 동일하지 않은 경우 컴파일 오류가 발생하지 않습니다. 이러한 이유로 우리는 mypy와 같은 외부 타입 체커를 사용하여 타입 오류를 식별합니다.
권장 사항
타이핑 모듈을 효과적으로 사용하기 위해서는 정적 타입 매칭을 확인하기 위해 외부 타입 체커/린터를 사용하는 것이 권장됩니다. Python에서 가장 널리 사용되는 타입 체커 중 하나는 mypy입니다. 따라서 기사를 읽기 전에 설치를 권장합니다.
이미 Python에서 타입 체크의 기본 사항을 다루었습니다. 이 기사를 먼저 읽어보세요.
이 기사에서는 정적 타입 체커로 mypy
를 사용할 것이며, 다음과 같이 설치할 수 있습니다:
pip3 install mypy
타입이 일치하는지 확인하기 위해 mypy
를 Python 파일에 실행할 수 있습니다. 이는 Python 코드를 ‘컴파일’하는 것과 같습니다.
mypy program.py
오류를 디버깅 한 후에는 다음과 같이 프로그램을 정상적으로 실행할 수 있습니다:
python program.py
이제 준비 사항이 완료되었으니, 몇 가지 모듈의 기능을 사용해 보겠습니다.
타입 힌트 / 타입 어노테이션
함수에 대해
함수의 반환 유형과 매개변수의 유형을 지정하기 위해 함수에 주석을 추가할 수 있습니다.
def print_list(a: list) -> None:
print(a)
이는 타입 체커(mypy
의 경우)에게 print_list()
라는 함수가 있으며 인수로 list
를 받아들이고 None
을 반환한다는 사실을 알려줍니다.
def print_list(a: list) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
먼저 이를 타입 체커 mypy
에서 실행해 보겠습니다:
vijay@JournalDev:~ $ mypy printlist.py
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)
예상대로, 5번째 줄의 인수가 int
가 아닌 list
이기 때문에 오류가 발생합니다.
변수에 대해
Python 3.6부터 변수의 유형을 주석으로 지정할 수도 있습니다. 그러나 함수가 반환되기 전에 변수의 유형이 변경되길 원한다면 이는 필수적이지 않습니다.
# 'radius'를 부동 소수점으로 주석 처리
radius: float = 1.5
# 값을 할당하지 않고 변수를 주석 처리할 수 있습니다!
sample: int
# 'area'를 부동 소수점으로 반환하는 것으로 주석 처리
def area(r: float) -> float:
return 3.1415 * r * r
print(area(radius))
# 함수의 모든 주석을 출력하기 위해
# '__annotations__' 사전을 사용합니다
print('Dictionary of Annotations for area():', area.__annotations__)
mypy의 출력 결과:
vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}
mypy
를 사용하는 권장 방법은 먼저 타입 어노테이션을 제공한 다음 타입 체커를 사용하는 것입니다.
타입 별칭
typing
모듈은 타입 별칭을 제공하며, 이는 타입에 별칭을 할당하여 정의됩니다.
from typing import List
# Vector는 실수 값들의 리스트입니다
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)
출력
vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]
위 코드 조각에서 Vector
는 리스트의 별칭으로 사용됩니다. 위 프로그램은 별칭에 타입 힌트를 주는 것입니다.
허용되는 별칭의 전체 목록은 여기에서 확인할 수 있습니다.
하나 더 예제를 살펴보겠습니다. 이 예제는 딕셔너리의 각 키:값 쌍을 확인하고 이름:이메일 형식과 일치하는지 확인합니다.
from typing import Dict
import re
# 'ContactDict'라는 별칭 생성
ContactDict = Dict[str, str]
def check_if_valid(contacts: ContactDict) -> bool:
for name, email in contacts.items():
# 이름과 이메일이 문자열인지 확인
if (not isinstance(name, str)) or (not isinstance(email, str)):
return False
# 이메일 형식 [email protected] 확인
if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
return False
return True
print(check_if_valid({'vijay': '[email protected]'}))
print(check_if_valid({'vijay': '[email protected]', 123: '[email protected]'}))
mypy에서의 출력
vijay@JournalDev:~ $ mypy validcontacts.py
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)
여기서 두 번째 딕셔너리의 name
매개변수가 정수 (123)인 경우 정적으로 컴파일 시간 오류가 발생합니다. 따라서 별칭은 mypy
에서 정확한 타입 체크를 강제하는 또 다른 방법입니다.
NewType() 함수를 사용하여 사용자 정의 데이터 타입을 생성할 수 있습니다.
새로운 사용자 정의 타입을 생성하기 위해 NewType()
함수를 사용할 수 있습니다.
from typing import NewType
# 'StudentID'라는 새로운 사용자 타입 생성
# 정수형으로 구성됨
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)
정적 타입 체커는 새로운 타입을 원래 타입의 하위 클래스로 취급합니다. 이는 논리적인 오류를 잡는 데 도움이 됩니다.
from typing import NewType
# 'StudentID'라는 새로운 사용자 타입 생성
StudentID = NewType('StudentID', int)
def get_student_name(stud_id: StudentID) -> str:
return str(input(f'Enter username for ID #{stud_id}:\n'))
stud_a = get_student_name(StudentID(100))
print(stud_a)
# 이는 잘못된 방법입니다!!
stud_b = get_student_name(-1)
print(stud_b)
출력 mypy에서 출력
vijay@JournalDev:~ $ mypy studentnames.py
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)
Any 타입
이는 모든 타입이 이 키워드와 호환되는 것을 정적 타입 체커(mypy
의 경우)에 알려주는 특별한 타입입니다.
이제, 어떤 타입의 인수도 받아들이는 print_list()
함수에 오류가 없습니다.
from typing import Any
def print_list(a: Any) -> None:
print(a)
print_list([1, 2, 3])
print_list(1)
이제, mypy
를 실행해도 오류가 발생하지 않습니다.
vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1
반환 타입이나 매개변수 타입을 지정하지 않은 함수는 자동으로 Any
를 사용하도록 기본 설정됩니다.
def foo(bar):
return bar
# 정적 유형 확인기는 위의 코드를
# 아래의 시그니처와 같게 처리합니다:
def foo(bar: Any) -> Any:
return bar
따라서 정적 및 동적으로 유형이 지정된 코드를 혼합하는 데 Any를 사용할 수 있습니다.
결론
이 글에서는 Python의 typing 모듈에 대해 알아보았습니다. 이 모듈은 유형 확인에 매우 유용하며 mypy
와 같은 외부 유형 확인기가 정확한 오류를 보고할 수 있게 합니다.
이를 통해 Python에서 정적으로 유형이 지정된 코드를 작성하는 방법을 제공받을 수 있습니다. Python은 동적으로 유형이 지정된 언어입니다!
참고 자료
- typing 모듈에 대한 Python 문서 (이 모듈의 더 많은 메서드에 대한 상세한 내용을 담고 있으며, 이를 보조 참고 자료로 추천합니다)
- 유형 힌트에 대한 StackOverflow 질문 (이 주제에 대한 매우 좋은 토론을 제공합니다. 꼭 읽어보시기를 추천합니다!)
Source:
https://www.digitalocean.com/community/tutorials/python-typing-module