단단한 파이썬 4. 타입의 제어
- 프로그래밍 언어/파이썬
- 2023. 10. 8.
들어가기 전
이 글은 단단한 파이썬 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
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
단단한 파이썬 9. 데이터 클래스 (0) | 2023.10.09 |
---|---|
단단한 파이썬 5. 컬렉션 타입 (1) | 2023.10.08 |
단단한 파이썬 3. 타입 어노테이션 (1) | 2023.10.08 |
전문가를 위한 파이썬 7. 함수 데코레이터와 클로저 (1) | 2023.10.06 |
단단한 파이썬 8장 : Enum 사용하기 (0) | 2023.09.14 |