Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[PyCon KR] Backend.AI에 점진적 typing 적용하기

Joongi Kim
September 26, 2020

[PyCon KR] Backend.AI에 점진적 typing 적용하기

Backend.AI 개발과정에서 Python의 새로운 기능인 type annotation을 활용하며 알게 된 것들을 정리해보았습니다.

Joongi Kim

September 26, 2020
Tweet

More Decks by Joongi Kim

Other Decks in Programming

Transcript

  1. Backend.AI에
    점진적 typing 적용하기
    김준기
    Lablup Inc.

    View Slide

  2. View Slide

  3. § CTO @ Lablup Inc.
    – Backend.AI 주 개발자 (2015년 공개)
    About Me
    @achimnol
    @achimnol

    View Slide

  4. § 타입 주석 개념과 도입 효과
    § 타입 주석을 활용하는 개발환경 준비하기 (VSCode, vim-lsp)
    § 코루틴과 제네레이터에 타입 주석 넣기
    § Protocol로 복잡한 타입 정의 및 조합하기
    § Generic으로 플러그인 시스템 설계하기
    § 동적 타입 검사를 보완적으로 활용하기 (JSON + trafaret)
    § Python 미래 버전의 타입 관련 기능 소개
    Contents

    View Slide

  5. [1] https://twitter.com/mmastrac/status/536332443398057984

    View Slide

  6. § Python은 타입이 없다? 약한 타입 언어다? No!
    § Python은 강타입 언어이며 동적 타입 언어이다.
    – Strong type: 오브젝트의 타입이 중간이 예측 불가능한 방식으로 변하지 않음
    – Dynamic type: 변수가 아닌 오브젝트에 타입이 지정됨
    üb = 1; b = 'a'가 가능한 이유 : 변수 b는 오브젝트를 가리키는 이름일
    뿐이며 1이나 'a'의 타입은 변하지 않음
    ü단점 : IDE나 타입검사 도구가 변수의 타입을 정적으로 결정하기 어려움
    (코드의 가능한 모든 분기를 분석하거나 코드를 실행해봐야 알 수 있음)
    Myth busting

    View Slide

  7. § PEP-484: Type Hints
    – https://docs.python.org/3/library/typing.html
    – 동적 타입인 Python 코드를 정적 타입(static typing)으로 작성하기 위한 명세
    – 함수 인자, 함수의 반환값, 변수 (property 포함) 타입 정의 가능
    – 말 그대로 주석이므로 런타임에 별도의 검사가 이뤄지지 않고 무시됨
    § 도입 효과
    – 여러 사람이 작성하는 규모가 큰 프로젝트에서 코드 유지보수가 쉬워짐
    – IDE나 type checker가 사전에 타입 불일치에 의한 버그를 발견할 수 있게 됨
    ü특히 값이 None인 경우에 대한 처리 빠뜨리는 코드
    – 읽을 거리 : https://dropbox.tech/application/our-journey-to-type-checking-4-
    million-lines-of-python
    타입 주석(type annotation)

    View Slide

  8. 타입 주석 예제
    def myatoi(arg1: int = None) -> str:
    ...
    class Document:
    title: str
    body: str
    metadata: Mapping[str, Any]
    parser: ClassVar[Parser]
    def somecode():
    ...
    q: asyncio.Queue[bytes] = asyncio.Queue()
    ...
    def somecode2():
    ...
    q: asyncio.Queue[bytes]
    q = asyncio.Queue()
    함수의 인자와 반환 타입 지정
    § 현존 static type checker 구현들은 인자 기본값이
    None인 경우 Optional 붙은 것으로 자동 해석함
    § mypy는 반환 타입이 지정되었을 때만 함수 본문의
    타입 검사를 수행함
    클래스·인스턴스 변수의 타입 지정
    § 명시하지 않은 경우, __init__() 메소드 내에서
    self의 속성에 할당하는 값의 형식에 따라 자동
    추론하기도 함
    일반 변수의 타입 지정
    § 특히 다른 변수들을 담을 수 있는 컨테이너(자료구조)
    타입에 대해서는 generic을 활용하여 하위 타입을
    정의할 수 있음
    일반 변수의 사용 전 타입 선언
    § 런타임에는 아무 기능을 하지 않는 코드이지만 별도의
    문장으로 타입 선언만 먼저 해두는 것도 가능함

    View Slide

  9. 타입 주석 활용한 구조체
    class Point(typing.NamedTuple):
    x: int
    y: int
    class Point(typing.TypedDict):
    x: int
    y: int
    @dataclass.dataclass
    class Point:
    x: int
    y: int
    @attr.s(auto_attribs=True)
    class Point:
    x: int
    y: int
    내장 dataclass 패키지 및 외부 attr 패키지
    § 인스턴스 변수의 타입 선언을 런타임에 읽어들여
    그에 맞는 타입을 갖는 생성자 및 속성 접근자를
    제공하는 구조체 클래스를 생성
    collections.namedtuple의 다른 선언 방법
    § 필드의 타입을 지정한 named tuple을 클래스로 선언
    § __annotations__ 속성이 추가 제공되어 필드별
    타입을 dict 형식으로 조회할 수 있음
    dict의 key별 타입을 고정하여 사용하는 방법
    § 런타임 동작은 일반 dict와 동일하지만 static type
    checker들이 키의 존재 유무 및 타입 검사가
    가능하도록 도와줌

    View Slide

  10. § VSCode + Python
    – Microsoft에서 공식 개발한 확장기능 제공
    타입 주석 활용 개발환경 만들기

    View Slide

  11. § vim/emacs + LSP plugins + python-language-server
    – python-language-server는 해당 프로젝트의 virtualenv 환경 안에 설치
    타입 주석 활용 개발환경 만들기

    View Slide

  12. § PyCharm
    – 타입 해석기·검사기 내장 및 편집 중 실시간 리포트 제공
    타입 주석 활용 개발환경 만들기
    https://blog.jetbrains.com/pycharm/2015/11/python-3-5-type-hinting-in-pycharm-5/

    View Slide

  13. 코루틴과 제네레이터 타입
    § 함수가 반환하는 값이 다른 값들을 생성하는 generator 및 iterator인 경우
    – 둘의 차이에 대해서는 관련 블로그 글 및 번역문을 참고!
    https://mingrammer.com/translation-iterators-vs-generators/
    § Iterator 자리에는 Generator를 넣을 수 있지만, Generator 자리에는 Iterator를
    항상 넣을 수는 없음
    Generator[YieldType, SendType, ReturnType]
    AsyncGenerator[YieldType, SendType]
    Iterator[YieldType]
    AsyncIterator[YieldType]
    Coroutine[YieldType, SendType, ReturnType] = Generator + __awaitable__()
    Awaitable[ReturnType]

    View Slide

  14. async 함수의 타입
    def myfunc() -> int:
    return 1
    async def myfunc() -> int:
    return 1
    Type check Result
    callable(myfunc), inspect.isfunction(myfunc) True
    inspect.iscoroutinefunction(myfunc) False
    type(myfunc)
    type(myfunc()) int
    Type check Result
    callable(myfunc), inspect.isfunction(myfunc) True
    inspect.iscoroutinefunction(myfunc) True
    type(myfunc)
    type(myfunc())
    inspect.iscoroutine(myfunc()) True
    type(await myfunc()) int

    View Slide

  15. async generator의 타입
    def myfunc(
    ) -> Generator[int, None, None]:
    yield 1
    async def myfunc(
    ) -> AsyncGenerator[int, None]:
    yield 1
    Type check Result
    callable(myfunc), inspect.isfunction(myfunc) True
    inspect.iscoroutinefunction(myfunc) False
    type(myfunc)
    type(myfunc())
    inspect.iscoroutine(myfync()) False
    inspect.isgenerator(myfunc()) True
    inspect.isasyncgen(myfunc()) False
    Type check Result
    callable(myfunc), inspect.isfunction(myfunc) True
    inspect.iscoroutinefunction(myfunc) False
    type(myfunc)
    type(myfunc())
    inspect.iscoroutine(myfunc()) False
    inspect.isgenerator(myfunc()) False
    inspect.isasyncgen(myfunc()) True
    Python generator에 대해서
    inpsect.iscoroutine()은 False이지만,
    개념적으로는 호출자와 generator 간의 양방향
    소통이 가능하다는 점에서는 corotuine으로
    간주할 수 있음. 다만 Python의 용어로는
    coroutine은 async 함수만을 뜻함.

    View Slide

  16. async context manager의 타입
    § contextlib.contextmanager는 한 번만 yield하는 Iterator를 감싸는 형태
    § contextlib.asynccontextmanager는 마찬가지로 AsyncIterator를 감쌈
    from contextlib import contextmanager
    @contextmanager
    def myctx(location: str) -> Iterator[Resource]:
    r = Resource.open(location)
    try:
    yield r
    finally:
    r.close()
    from contextlib import asynccontextmanager
    @asynccontextmanager
    async def myctx(location: str) -> AsyncIterator[Resource]:
    r = await Resource.open(location)
    try:
    yield r
    finally:
    await r.close()

    View Slide

  17. § PEP-544: Protocols: Structural subtyping (static duck typing)
    – Mixin처럼 특정 메소드들의 부분적 인터페이스 요건만 따로 정의할 수 있음
    – 하나의 오브젝트가 여러 Protocol을 구현할 수 있음 (=조합할 수 있음)
    – 예시) Iterable, Iterator, Callable, Sized, Hashable, ...
    Protocol로 부분적 타입 정의하기
    from typing import Protocol
    class Closable(Protocol):
    def close(self) -> None: pass
    def close_all(
    things: Iterable[Closable]
    ) -> None:
    for t in things:
    t.close()
    f = open('foo.txt')
    close_all([f]) # OK!
    close_all([1]) # Error: 'int' has no 'close' method

    View Slide

  18. § 가장 큰 활용처 : Callback 함수의 signature가 복잡할 경우 Protocol 클래스의
    __call__ dunder method를 활용할 수 있음
    Protocol로 "복잡한" 타입 정의하기
    from typing import Protocol
    class MyCallback(Protocol):
    def __call__(
    self,
    context: Context, *,
    options: Mapping[str, Any] = None,
    ) -> None:
    pass
    from typing import Callable
    MyCallback = Callable[[Context, ?], None]
    keyword argument를 표현할 방법이 없다?!
    Protocol 클래스를 활용하면 keyword argument를
    포함한 callable 인터페이스를 정의할 수 있다!

    View Slide

  19. § Generic + bound TypeVar 이용해서 type hierarchy 구성
    Generic으로 플러그인 설계하기
    from typing import TypeVar, Generic, ...
    P = TypeVar('P', bound=AbstractPlugin)
    class BasePluginContext(Generic[P]):
    ...
    @classmethod
    def discover_plugins(
    cls,
    plugin_group: str,
    blocklist: Container[str] = None,
    ) -> Iterator[Tuple[str, Type[P]]]: ...
    class HookPluginContext(BasePluginContext[HookPlugin]):
    ...
    from abc import ABCMeta
    class AbstractPlugin(metaclass=ABCMeta):
    ...
    class HookPlugin(
    AbstractPlugin, metaclass=ABCMeta
    ):
    ...
    class ConcreteHookPlugin(HookPlugin):
    ...

    View Slide

  20. § 정적 타입은 Python의 클래스/변수에만 적용 가능
    – JSON, msgpack과 같은 경우는 어떻게 할까?
    § 런타임에 타입 검사하기
    – typeguard : 타입 주석을 런타임에 읽어들여 검사
    – trafaret : 임의의 직렬화 데이터를 런타임에 구조 검사
    동적 타입 검사 기법(feat. JSON)
    import datetime
    import trafaret as t
    date = t.Dict(year=t.Int, month=t.Int, day=t.Int) >> (lambda d: datetime.datetime(**d))
    assert date.check({'year': 2012, 'month': 1, 'day': 12}) == datetime.datetime(2012, 1, 12)
    (example from https://github.com/Deepwalker/trafaret/)

    View Slide

  21. § REST API의 JSON body 또는 query string으로 전달된 인자 검사 및 변환
    Backend.AI의 trafaret 활용
    @attr.s(auto_attribs=True, slots=True, frozen=True)
    class DestroySessionParams:
    session_id: str
    @classmethod
    def as_trafaret(cls) -> t.Trafaret:
    return t.Dict({t.Key('session_id'): t.String})
    async def destroy_session(
    request: web.Request
    ) -> web.Response:
    params = await check_params(request, DestroySessionParams)
    await do_destroy(params.session_id)
    return web.Response(status=204)
    async def check_params(request, param_cls):
    try:
    raw_params = await request.json()
    checked_params = param_cls.as_trafaret() \
    .check(raw_params)
    return param_cls(**checked_params)
    except t.DataError as e:
    raise web.HTTPInvalidRequest(body=json.dumps({
    "title": "Invalid API parameters",
    "type": "https://api.backend.ai/probs"
    "/invalid-api-params",
    "data": e.as_dict(),
    }), content_type="application/problem+json")
    t.DataError는 다중 key를 가진 t.Dict에서
    여러 key에서 오류가 발견될 경우 모든 오류 key에
    대한 오류 설명을 포함하고 있기 때문에 디버깅 및
    사용자 친화적 오류 UX 설계에 도움이 됨

    View Slide

  22. § TOML/etcd로부터 읽어들인 config 검사 및 변환
    Backend.AI의 trafaret 활용
    local_config_iv = t.Dict({
    t.Key('agent'): t.Dict({
    t.Key('node_id'): t.String,
    t.Key('service_addr', default=('127.0.0.1', 6001)): tx.HostPortPair,
    }).allow_extras('*'),
    }).allow_extras('*') # for forward-compatibility
    ※ tx는 ai.backend.common.validators 모듈[1]의
    alias로 trafaret이 기본제공하지 않는 확장 변환기들을 추가 제공함
    (예: UUID, Slug, TimeDuration, JsonWebToken 등)
    raw_config = toml.loads("agent.toml")
    try:
    config = local_config_iv.check(raw_config)
    except t.DataError as e:
    raise ConfigurationError(e.as_dict())
    ...
    [1] https://github.com/lablup/backend.ai-common/blob/master/src/ai/backend/common/validators.py

    View Slide

  23. § PEP-563: Postponed Evaluation of Annotations
    – 모듈 로딩 시 타입주석을 AST 구문분석만 하고 실제로 실행하지 않음
    – Python 3.7부터 __future__.annotations 불러와 활성화 가능
    (Python 3.10에서 기본으로 활성화)
    § PEP-585: Type Hinting Generics In Standard Collections
    – Python 3.9 주요 신기능!
    – 내장 컨테이너 타입들에 제네릭 요소 타입 지정 기능
    곧 도입될 기능들
    from typing import List
    a: List[int]
    a: list[int]

    View Slide