YOGYUI

Python::typing::Literal - 변수값 범위 명시 본문

Software/Python

Python::typing::Literal - 변수값 범위 명시

요겨 2022. 1. 24. 13:33
반응형

typing - Literal

 

파이썬, 자바스크립트, 루비 등 최근 유행하는 프로그래밍 언어의 가장 큰 특징 중 하나는 변수의 형(type)을 지정하지 않아도 원활하게 동작하는 코드를 작성할 수 있다는 점이다 (dynamic typing)

v = '123456789'
>> type(v)
<class 'str'>

v = 123456789
>> type(v)
<class 'int'>

하지만 대형 팀 프로젝트 작업 시에 함수 인자 및 반환의 형을 명시해두지 않으면 각종 예외 발생으로 고통받게 된다

def my_func(x):
    return x + 1

>> my_func(1)
2

>> my_func('123')
Traceback (most recent call last):
  File "C:\Python38\lib\code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 3, in <module>
  File "<input>", line 2, in my_func
TypeError: can only concatenate str (not "int") to str

이러한 문제를 해결하기 위해 python 3.5 버전부터 typing 패키지가 추가되었다

위 함수는 다음과 같이 변수 타입과 반환 타입을 명시해줄 수 있다

def my_func(a: int) -> int:
    return a + 1

PyCharm과 같은 통합 개발 환경(IDE)에서 잘못된 타입의 변수를 대입할 때 warning 메시지를 확인할 수 있으므로 런타임 오류 가능성을 크게 줄일 수 있다

typing 패키지 내에 선언되어 있는 특수형과 관련된 클래스를 import하여 변수형을 더욱 명시적으로 만들 수도 있다

from typing import Tuple

def my_func2(a: tuple) -> int:
    return a[0] + a[1]

def my_func3(a: Tuple[int, int]) -> int:
    return a[0] + a[1]

>> my_func2((1,2))
3
>> my_func3((1,2))
3

my_func2, my_func3 모두 길이 2 (이상의)요소들로 구성된 튜플을 대상으로 하여 동일한 결과를 도출하는데, my_func3의 경우 튜플이 int형 2개만 갖고 있도록 명시하고 있어 코딩 오류를 억제할 수 있다

 

중·대형 프로젝트에서는 왠만하면 자료형을 명시해서 사소한 실수로 인한 런타임 오류를 억제하도록 하는게 좋다

Alias 클래스로 제공되는 Tuple, List, Dict, Any, Union 등은 여러모로 유용하게 활용할 수 있으니 활용법은 python 공식 문서를 참고하도록 하자

https://docs.python.org/3/library/typing.html

 

typing — Support for type hints — Python 3.10.2 documentation

Note The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. This module provides runtime support for type hints. The most fundamental support consists of

docs.python.org


Python 3.8에 처음 도입된 Literal 클래스를 활용하면 변수가 가질 수 있는 값의 범위를 명시적으로 제한할 수 있다

from typing import Literal

class Student:
    grade: Literal[1, 2, 3, 4]

    def __init__(self):
        pass

    def setGrade(self, g: Literal[1, 2, 3, 4]):
        self.grade = g

학부생의 학년(grade)을 1~4로만 제한하고자 할 때, Literal을 활용하면 if문 사용하지 않고 사용자 오류를 억제할 수 있다

 

Literal로 활용할 수 있는 자료형은 int, str, bytes, bool, enum.Enum, None 그리고 Literal이 있다

물론 함수 혹은 메서드의 반환형에도 사용할 수 있다

class Student:
    grade: Literal[1, 2, 3, 4]
    gender: Literal['male', 'female', 'unspecified']
    scholarship: Literal['all', 'half', None]
    
    def getGender(self) -> Literal['male', 'female', 'unspecified']:
        return self.gender
    
    def setGender(self, g: Literal['male', 'female', 'unspecified']):
        self.gender = g

>> s1.setGender('female')
>> s1.getGender()
'female'

위와 같이 구현하면 grade, gender, scholarship 멤버변수가 가질 수 있는 값의 범위를 명시적으로 제한할 수 있어 IDE를 통한 사전 오류 체크가 가능하며, 다른 사람이 코드를 해석할 때 큰 도움을 줄 수 있다

 

물론 python 언어 자체가 가지는 특성 때문에 명시되지 않은 값을 대입할 수 있기는 하다 (IDE상 경고메시지만 출력할 뿐 실제 실행 시 런타임 오류는 발생하지 않는다)

>> s1.setGender('unknown')
>> s1.getGender()
'unknown'

Literal로 명시적으로 값의 범위를 명시해두고 함수 내에서 조건문을 통해 예외를 발생하게 하는게 일반적인 구현 방법

class Student:    
    def setGender(self, g: Literal['male', 'female', 'unspecified']):
        if g in ['male', 'female', 'unspecified']:
            self.gender = g
        else:
            raise ValueError('invalid gender')
>> s1.setGender('unknown')
Traceback (most recent call last):
  File "C:\Python38\lib\code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
  File "<input>", line 13, in setGender
ValueError: invalid gender

나도 주로 3.7로 작업하다보니 Literal을 접할 기회가 별로 없었는데, 빌드 서버 이슈로 3.8을 도입해보니 기존에 구현해둔 int.to_bytes 구문에 전부 warning이 잡혀있길래 빌트인 소스코드(builtins.py)를 열어봤더니

class int:
    @overload
    def __new__(cls: Type[_T], x: str | bytes | SupportsInt | SupportsIndex | _SupportsTrunc = ...) -> _T: ...
    @overload
    def __new__(cls: Type[_T], x: str | bytes | bytearray, base: SupportsIndex) -> _T: ...
    if sys.version_info >= (3, 8):
        def as_integer_ratio(self) -> tuple[int, Literal[1]]: ...
    @property
    def real(self) -> int: ...
    @property
    def imag(self) -> int: ...
    @property
    def numerator(self) -> int: ...
    @property
    def denominator(self) -> int: ...
    def conjugate(self) -> int: ...
    def bit_length(self) -> int: ...
    if sys.version_info >= (3, 10):
        def bit_count(self) -> int: ...
    def to_bytes(self, length: SupportsIndex, byteorder: Literal["little", "big"], *, signed: bool = ...) -> bytes: ...
    @classmethod
    def from_bytes(
        cls, bytes: Iterable[SupportsIndex] | SupportsBytes, byteorder: Literal["little", "big"], *, signed: bool = ...
    ) -> int: ...  # TODO buffer object argument

to_bytes와 from_bytes의 byteorder가 Literal로 잡혀있어서 "little", "big" 두 종류의 endian 관련 문자열만 입력받을 수 있다는 것을 명시하고 있는 것을 알 수 있다


dynamic typing이 파이썬의 장점이라고는 하지만, 중·대형 팀 프로젝트에서는 변수를 중구난방으로 사용할 수 있다는 치명적인 약점이 되어버리기에 버전이 올라감에 따라 사용자들의 요구에 의해 typing 그리고 literal까지 도입되고 있는 것을 알 수 있다

 

python 3.8 이상 버전으로 작업하는 개발자라면, typing을 도입해서 코드를 해석하기 좋게 만들어서 협업하기 좋은 환경을 만들어보자 (아무리 최신식 협업툴을 갖춰도 소스코드 자체가 읽기 불편하면 말짱 도루묵이다)

> static typing은 clean code를 위한 첫걸음이기도 하다

끝~!

 

[참고]

[1] https://docs.python.org/3/library/typing.html

[2] https://adamj.eu/tech/2021/07/09/python-type-hints-how-to-use-typing-literal/

반응형