StatCounter - Free Web Tracker and Counter

단단한 파이썬 4. 타입의 제어

반응형

들어가기 전

이 글은 단단한 파이썬 4장을 공부하며 작성한 글입니다. 

 


고급 타입 어노테이션

기본적인 타입 어노테이션만을 이용하는 것은 한계가 존재한다. 파이썬에서는 고급 타입 어노테이션을 제공하는데, 이것을 이용하면 좀 더 단단한 코딩을 할 수 있다.

  • Optional : Union[None, 내 타입]을 의미함.
  • Union : 여러 타입들이 선택될 수 있음을 의미
  • Literal : 개발자가 특정 값들만 사용하도록 제약
  • Annotated : 여러분의 타입에 추가적인 설명을 제공하고자 사용
  • NewType : 특정 상황에서 타입을 제한하고자 사용
  • Final : 불변 타입임을 알려줌. 

아래에서는 하나씩 내용을 살펴보고자 한다. 

 

 


Optional 타입

Optional 타입을 정리하면 다음과 같다.

  • Optional은 Union[내 타입, None]을 의미함. 
  • 이 변수를 사용하는 사람이 명시적으로 None에 대해서 처리해야함을 알려줌. 
  • Collection에 None을 사용하면 더욱 의미가 명확함.
    • 비지 않은 리스트 : 가용한 데이터가 있음.
    • 빈 리스트 : 가용한 데이터가 없음.
    • None : 오류 케이스
  • 타입 체커를 통해서 타입체크를 하는 경우, 좀 더 디테일한 내용을 알려줌. (아래 코드에서 확인) 
from typing import Optional

class Bun:
    def __init__(self, name: str):
        self.name = name
        self.ham: list[str] = []

    def add_ham(self, ham: str):
        self.ham.append(ham)


def not_available():
    return True


def get_bun() -> Optional[Bun]:
    if not_available():
        return None
    return Bun('Wheat')


def create_hot_dog():
    bun = get_bun()
    bun.add_ham("ham")

파이썬의 Optional은 이 변수를 사용하는 사람이 None(null)에 대해서 대응해야 함을 명시적으로 알려주는 역할을 한다. 여기서 mypy를 어떻게 실행하느냐에 따라 여러가지 결과가 나오는 것을 확인할 수 있다.

$ mypy --strict 4-1.py   
>>>
...
4-1.py:24: error: Item "None" of "Optional[Bun]" has no attribute "add_ham"  [union-attr]

$ mypy 4.1py
>>>
Success: no issues found in 1 source file

첫번째 결과는 get_bun()의 결과 Optional[Bun]이 반환되는데, 이 때 bun이 None이라면 add_ham()이라는 Attribute가 없음을 의미한다. 만약 이런 에러를 해결하고 싶다면 None 케이스를 처리하는 코드를 작성해야한다.

def create_hot_dog():
    bun = get_bun()
    if bun is None:
        return
    bun.add_ham("ham")
    
    
$ mypy --strict 4-1.py   
>>>
...
Success: no issues found in 1 source file

None에 대한 방어적 코드를 작성하면 이 문제가 해결되는 것을 확인할 수 있다. 

 


Union 타입

  • Union 타입은 한 변수에 여러 타입이 올 수 있음을 의미함. 
  • Union을 이용한 다중 상속을 이용하지 않고도 간단히 타입 어노테이션을 할 수 있음. Union[Hotdog, Pretzel]
  • Union의 Usecase
    • 사용자 입력에 따라 서로 다른 반환 타입을 가질 때
    • 사용자 입력으로 다른 형태의 타입을 받을 수 있을 때 
    • 이전 버전과의 호환성을 위해 다른 타입을 반환함. 
    • 하나 이상의 값을 어쩔 수 없이 다뤄야 하는 경우 
from typing import Union


class Hotdog:
    def __init__(self):
        print('Hotdog')


class Ham:
    def __init__(self):
        print('Ham')


def get_func(name: str) -> Union[Hotdog, str]:
    if name == 'Hotdog':
        return Hotdog()
    if name == 'Ham':
        return Ham()
    return 'Other'

Union 타입은 하나의 변수에 여러 가지 타입이 올 수 있을 때 주로 사용한다. 위에서는 반환값으로 Hotdog, str 타입이 올 수 있음을 의미한다. 

$ mypy 4-2.py
4-2.py:15: error: Incompatible return value type (got "Ham", expected "Union[Hotdog, str]")  [return-value]
Found 1 error in 1 file (checked 1 source file)

타입 체커를 실행해보면 경고가 발생하는 것을 알 수 있다. Hotdog, str 타입만 반환할 수 있는데 이 과정에서 Ham 타입이 반환되는 경우도 존재하기 때문이다. 

 


Literal 타입

  • Literal 타입은 매개변수의 값을 특정 범위로 제한하는데 사용한다.
from typing import Literal
from dataclasses import dataclass


@dataclass
class Error:
    error_code: Literal[1, 2, 3, 4, 5]
    diposed_of: bool


Error("a", True)
Error(1, True)


위의 코드가 Literal의 예시가 된다.

$ mypy 4-3.py
4-3.py:11: error: Argument 1 to "Error" has incompatible type 
"Literal['a']"; expected "Literal[1, 2, 3, 4, 5]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

위 코드에 대해서 타입체커를 실행해보면 다음 결과가 나온다. 1,2,3,4,5 중에 하나의 값만 나와야 하는데 "a"라는 값이 포함되었기 때문에 문제가 된다는 것을 타입체커가 발견한다. 

 


Annotated 타입

  • 변수에 가능한 값을 수백개로 제한할 때, Literal에 일일이 모든 값을 작성하기는 어렵다. 가독성도 좋지 않고 낭비 일 것이 분명하기 때문이다. 
  • Literal은 문자열의 크기, 특정 정규 표현에 맞게 제약을 할 수는 없음. 
  • Annotated 타입을 위한 타입체커가 존재하지 않기 때문에 문제를 검출할 수는 없음. (알려주는 용도로 사용함)
from typing import Annotated

def ValueRange(startInt, endInt):
    return True

x: Annotated[int, ValueRange(3, 5)]
y: Annotated[int, "범위는 3부터 5까지만 가능."]

 

 


NewType

Annotated 타입을 위한 타입체커는 존재하지 않다. Annotated 타입을 써야하고 타입 체커를 사용하고 싶다면 NewType을 사용하는 것도 좋은 방법이 될 수 있다. 

  • NewType은 기존의 타입을 받아 동일한 필드 + 메서드를 갖는 새로운 타입을 생성함. 
  • NewType은 묵시적 타입 전환을 허용함. NewType은 단방향 타입 변환만 지원함. 
    • 기존 타입 -> 새로운 타입은 전환 안됨.
    • 새로운 타입 -> 기존 타입은 전환됨. 
  • NewType의 묵시적 단방향 타입 전환 지원 덕분에 다음 UseCase를 가질 수 있음.
    • User 객체와 LoggedInUser 객체를 분리해서 추적한다. NewType('LoggedInUser', User)
    • 유효 사용자 ID를 나타내야 하는 정수 값을 추적한다. 사용자 ID를 NewType으로 제한함으로써 If문의 사용 없이 일부 함수가 유효한 ID로만 동작하게 할 수 있음.
from typing import NewType


class Hotdog:
    ''' 준비가 안된 핫도그를 나타냄. '''

# NewType 생성. 클래스 생성된 것처럼 보고 생성자로 생성하면 됨. 
ReadyHotdog = NewType('ReadyHotdog', Hotdog)


def server_to_customer1(hotdog: ReadyHotdog):
    pass


def server_to_customer2(hotdog: Hotdog):
    pass

my_hotdog = Hotdog()
my_ready_dog = ReadyHotdog(my_hotdog)


### 단방향 타입 전환만 가능
server_to_customer1(my_hotdog) # 오류 발생
server_to_customer1(my_ready_dog) # 통과

server_to_customer2(my_hotdog)
server_to_customer2(my_ready_dog)
  • NewType() 메서드를 이용하면 해당 타입을 한번 더 랩핑한 새로운 타입을 만들어 낼 수 있다. 위 코드에서는 Hotdog를 한번 더 랩핑해서 ReadyHotDog라는 새로운 타입을 생성한다.
  • NewType으로 생성된 타입은 타입 어노테이션 관점에서 단방향 전환을 지원한다. 이 말은 Hotdog -> ReadyHotdog로는 안되지만, ReadyHotdog -> Hotdog는 지원한다는 것을 의미한다. 
$ mypy 4-5.py
4-5.py:21: error: Argument 1 to "server_to_customer1" has incompatible type "Hotdog"; expected "ReadyHotdog"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

타입 체커를 실행해보면 단방향 타입 전환만 지원하는 것을 알 수 있다. 위 코드에서 오류 발생 부분에서 타입체커의 경고가 불을 뿜는다. 

from typing import NewType, Union

class User:
    def __init__(self, name: str, login: bool):
        self.login = login
        self.name = name

LoginUser = NewType('LoginUser', User)

# LoginUser 객체를 생성하는 메서드를 작성.
def login(user: Union[User, LoginUser]
          ) -> Union[User, LoginUser]: 
    if user.name == 'login':
        return LoginUser(user)
    return user

NewType과 타입 체커를 적극적으로 이용하기로 했으면, NewType의 객체를 생성하는 메서드를 하나 만들어야 한다. 이런 메서드에서는 필요한 유효성 검사를 한 후에 객체를 만들어주는 것이다. 한 가지 문제점은 파이썬에서는 자바처럼 private 접근 지시자가 없기 때문에 강제할 수 없다는 것이다. 즉, deligation을 통해 의도와는 다르게 사용될 가능성이 남아있다는 것을 인지해야한다.

 

 


Final 타입

불변 타입을 나타내기 위해서 Final을 사용할 수 있음. 

  • 타입 체커는 Final에 대한 타입 체킹을 처리해준다. 
  • Final은 모듈처럼 변수의 사용 범위가 매우 넓을 때 많이 사용됨.
from typing import Final

def display_vendor():
    VENDOR_NAME: Final = "My Name"
    VENDOR_NAME += "A"

위는 불변 타입 어노테이션 Final을 이용한 코드다. 

$ mypy 4-7.py
4-7.py:4: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
4-7.py:5: error: Cannot assign to final name "VENDOR_NAME"  [misc]
Found 1 error in 1 file (checked 1 source file)

위 코드에 대해 타입 체커를 실행해보면 final name의 값을 바꿀 수 없다는 에러가 발생하는 것을 확인할 수 있다.

 


타입 별칭

필요한 경우 타입 별칭을 이용할 수도 있다. 타입 별칭을 이용하는 것이 가독성이 더 좋다면 타입 별칭을 적극적으로 사용해 볼 수도 있다. 그러나 타입 별칭을 찾아보기 위해서 한번 더 코드 점프를 해야하는 단점이 있으므로 이런 것들을 잘 고려해봐야 한다.

from typing import Union

# 타입 별칭 붙이기. 
IdOrName = Union[int, str]

def is_Id_or_name(my_string: IdOrName):
    pass

댓글

Designed by JB FACTORY