견고한 Python - 1일차 Type annotations

0

robust_python

목록 보기
1/5

https://www.yes24.com/Product/Goods/111419833

영어로는 Robust Python인데, 한국어로 단단한(?) python으로 해석되어 출판되었다. 아무리봐도 '단단한'보다는 '견고한'이 맞는 것 같다.

파이썬에 type을 표시하는 일은 매우매우 중요하다. 누군가는 python의 type을 표시하면 python의 동적 타입 언어라는 장점이 사라진다는 데, 전혀 아니다.

type표시를 해도 동적 타입 언어라는 것은 분명하다. 만약, type표시가 불만이라면 golang을 한 번 해보고 와라. 강타입 언어의 맛을 보면, python에 타입 표시 하나하는게 그닥 어렵지 않을 것이다.

협업을 하는 데 있어서 python의 type은 매우 중요하다. 누군가가 마구마구 만들어낸 python code를 타입 힌트도 없이, 매번 상위 호출을 따라들어가 타입을 확인하는 것은 불편한 일이다.

이제 python에 type hint를 붙이고 광명을 찾아보도록 하자.

Type Annotations

파이썬은 동적 타입 언어으로 runtime전까지는 타입을 알 수 없다. 이러한 부분은 python을 개발할 때 견고한 코드를 만들게하기 어려운 부분 중 하나이며, 디버깅을 매우 어렵게 만든다. 따라서, python으로 개발할 때는 나름의 규칙을 정해야하는데, python에서 type에 대한 규칙이 바로 type annotation인 것이다.

type annotations란?

python의 특정 변수에 대해서 어떤 type인지 알려줄 수 있는 기능이 python3.6부터 추가되었다. str, int, float, list, tuple, dict 등의 타입을 변수 옆에 적어주면 된다.

a: int = 10
b: str = "hello"
c: list = []

variable: type으로 적는 것이 바로 type annotation이다. 기본 type뿐만 아니라 class도 타입으로 사용이 가능하다.

다음의 코드를 확인하도록 하자.

def close_kitchen_if_past_close(point_in_time: datetime.datetime):
    if point_in_time >= closing_time():
        close_kitchen()
        log_time_closed(point_in_time)

위 코드에서 type annotation을 사용한 부분은 datetime.datetime이다. 이는 class로 하나의 타입이 된 것이다.

python에서의 'type annotation'은 그저 type에 대한 hint이다. 이는 해당 변수에 기대되는 값을 설명하는 부분으로, runtime에는 영향을 미치지 않아 사실상 지키지 않아도 된다. 즉, 주석과도 같지만, 단지 개발할 때 hint를 주는 것이다.

또한, python의 경우는 duck typeing을 지원하기 때문에, type이 가진 functionality가 동일하면 문제없이 호환된다. 다만, 일부러 type annotation을 어기는 일은 없도록 하자. 이는 버그를 유도하도록 하는 코드를 만들기 때문이다.

type annotation을 사용하면 다음의 장점을 얻을 수 있다.
1. 개발자는 자신의 코드를 명확히하고 디버깅하기 쉬워진다.
2. reader는 더욱 쉽게 코드를 전체적으로 파악할 수 있고, 인수인계가 쉬워진다.
3. 사용자는 코드에 필요한 type들을 알게되어, 코드를 사용할 때 호환성 문제를 최소화할 수 있다.

함수의 파라미터 뿐만 아니라, 함수의 반환값에서도 type annotation을 설정할 수 있다.

def fake_func() -> <type>:

다음과 같이 <type>부분을 원하는 type으로 변경하면 된다.

가령, 'str'을 담은 'list'를 반환한다고 하면 다음과 같다.

def find_workers_available_for_time(open_time: datetime.datetime) -> list[str]:

이렇게 작성하면, 사용자나 코드를 분석하는 사람은 반환값이 무엇인지 알기위해 코드를 하나하나 분석할 필요가 없다.

참고로, python3.8부터는 built-in collection types를 제공하는 데, List, Dict 같은 것들을 제공한다. 이는 listdict와 같은 소문자와 bracket이 함께 나오는 타입을 사용하지 않게 하기 위함이다. 즉, list[str]이 아니라 List[str]을 쓰게 하고싶어서 만들어졌다.

from typing import Dict,List
AuthorToCountMapping = Dict[str, int]
def count_authors(
                   cookbooks: List[Cookbook]
                 ) -> AuthorToCountMapping:
    # ...

당연하게도 type annotation은 변수에도 사용할 수 있다.

workers: list[str] = find_workers_available_for_time(open_time)
numbers: list[int] = []
ratio: float = get_ratio(5,3)

number: int = 0
text: str = "useless"
values: list[float] = [1.2, 3.4, 6.0]
worker: Worker = Worker()

한 가지 조심해야할 것은 모든 곳들에 type annotation을 난무할 필요가 없다. type annotation은 그저 type hint를 위한 것이고, type annotation에 너무 집중하여 코드를 복잡하게 만들 필요없다.

type annotation의 장점

가장 먼저 Autocomplete이다. python은 동적 타입 언어이기 때문에, 동작에 있어서 어떤 일이 발생할 지 예측하기가 어렵다. 이때 type hint를 제공하는 type annotation이 있다면 ide에서 자동완성을 도와주기 때문에 많은 부분에서 도움을 얻을 수 있다.

다음은 Typecheckers이다. type annotation이 제공해주는 type hint는 말 그대로 type을 지키지 않아도 되는 hint일 뿐이다. 따라서, type에 대한 엄격한 check가 없어서 다른 type을 넣어주는 버그를 만들더라도 runtime에 해당 코드가 실행되기 이전까지 버그가 발생하지 않는다. 만약, 잘못된 type을 입력 및 반환하면 runtime이전에 에러가 발생하도록 하고 싶다면 Typecheckers을 사용하면 된다.

다음의 예제를 보도록 하자.

a: int = 5
a = "string"
print(a) # "string"

a변수의 type이 int였다가 "string"을 받고 str이 되었다. 이러한 부분들은 코드를 디버깅하고 개발하는 데 있어 큰 걸림돌이 된다. test단계에서 이러한 부분들을 검출해주고 type에 대한 규약을 어기지 않았는 지 체크하는 tool이 바로 typechecker이다. 이는 static analysis tool과 과도 비슷하다.

가장 유명한 typechecker인 mypy를 사용해보도록 하자.

pip3 install mypy

다음으로 위의 예제를 python code로 만들어보도록 하자.

  • invalid_type.py
a: int = 5
a = "string"
print(a)

해당 code를 mypy로 분석해보도록 하자.

mypy invalid_type.py 

invalid_type.py:2: error: Incompatible types in assignment (expression has type "str", variable has type "int")  [assignment]
Found 1 error in 1 file (checked 1 source file)

에러를 잘보면 int인 것을 str로 바꿨다고 알려준다. 해당 부분을 수정하도록 하자.

  • invalid_type.py
a: int = 5
a = 10
print(a)

이제 mypy를 돌려주도록 하자.

mypy invalid_type.py 
Success: no issues found in 1 source file

성공한 것을 볼 수 있다. 이처럼 mypy와 같은 tool을 typechecker라고 하며 사용자의 의도와는 다르게 type을 오용하는 것을 막아주고, 잠재 문제를 걸러주는 기능을 한다.

몇 가지 예시를 보도록 하자.

  • invalid_example1.py
def read_file_and_reverse_it(filename: str) -> str:
    with open(filename) as f:
        # Convert bytes back into str
        return f.read().encode("utf-8")[::-1]

다음의 코드는 무엇이 문제일까?? 한 번 유추해보고 mypy로 결과를 확인해보도록 하자.

mypy invalid_example1.py 
invalid_example1.py:4: error: Incompatible return value type (got "bytes", expected "str")  [return-value]
Found 1 error in 1 file (checked 1 source file)

반환값이 str이 아니라 bytes가 나왔다고 한다. 이는 encode가 아니라 사실 decode를 해주어야 하기 때문이다.

다음은 무엇이 문제일지 생각해보자.

  • invalid_example2.py
# takes a list and adds the doubled values
# to the end
# [1,2,3] => [1,2,3,2,4,6]
def add_doubled_values(my_list: list[int]):
    my_list.update([x*2 for x in my_list])

add_doubled_values([1,2,3])

mypy를 구동하면 다음과 같다.

mypy invalid_example2.py 
invalid_example2.py:2: error: "list[int]" has no attribute "update"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

update가 없다고 나오는 데 update는 사실 list의 method가 아니라 set의 method이다. 즉, list에게 없는 attributes를 호출했기 때문에 mypy가 error로 표현한 것이다.

다음은 무엇이 문제인지 생각해보도록 하자.

  • invalid_example3.py
# The restaurant is named differently
# in different parts of the world
ITALY_CITIES = "italy"
GERMANY_CITIES = "germany"
US_CITIES = "us"

def get_restaurant_name(city: str) -> str:
    if city in ITALY_CITIES:
            return "Trattoria Viafore"
    if city in GERMANY_CITIES:
            return "Pat's Kantine"
    if city in US_CITIES:
            return "Pat's Place"
    return None


if get_restaurant_name('Boston'):
    print("Location Found")

mypy의 결과는 다음과 같다.

mypy invalid_example3.py 
invalid_example3.py:14: error: Incompatible return value type (got "None", expected "str")  [return-value]
Found 1 error in 1 file (checked 1 source file)

이 부분은 사실 조금 미묘한 문제이다. 문자열을 반환하는데 매칭되는 것이 없다면 None을 반환하는 것이 문제가 되진 않기 때문이다. 즉, if문으로 None일 때를 처리할 수 있기 때문이다.

그러나 이런 경우는 Nonestr 둘 중 하나가 반환된다고 표현해야하는 것이 맞다. 만약, 사용자가 get_restaurant_name 함수의 시그니처만 보고 str이 반환된다고 알고있으면 str에 대한 method들을 호출하려고 할 수 있다. 이 때 None인지 아닌지에 대한 검사를 생략할 수 있으며, 이런 경우 runtime이 에러가 발생하게 되고 디버깅하기 매우 어려운 상황을 맞이할 수 있다.

0개의 댓글