본문 바로가기
Coding/Python

Python Generic Type

by Hide­ 2022. 7. 7.
반응형

개요

스프링에서는 Spring Data JPA라는 것이 존재한다. 해당 라이브러리는 인터페이스를 생성 후 특정 인터페이스를 상속받으면 자동으로 메소드가 생성되는 역할을 한다. 예를 들어 findById(), findByEmail(), save(), delete() 등 여러가지 CRUD관련 메소드가 자동으로 생성된다. 

실무에서 파이썬을 통해 서버를 개발하며 Layered architecture를 도입해서 사용하고 있는데, 특정 레포지토리를 생성할 때 마다 동일한 역할을 하는 메소드를 만드는게 귀찮았다. 그래서 스프링의 Data JPA와 같은 것을 만들어볼 순 없을까 생각했고 우연찮게 FastAPI 를 만든 개발자의 깃헙에서 비슷한 코드를 발견하였다. (https://github.com/tiangolo/full-stack-fastapi-postgresql/blob/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/app/crud/base.py)

위 코드를 보면 Generic 타입을 활용하여 여러곳에 type hinting을 진행해주고 있는데, 처음봤을때는 정확하게 어떠한 뜻인지 이해가 가지 않았다. 파이썬에서는 제네릭이란 개념이 생소하기 때문인 것 같다. 본 포스팅에서는 typing 모듈에서 제공해주는 제네릭 타입에 대해 공부한 내용을 담아본다.

Usage

def get_sorted_arr(arr):
    return sorted(arr)


arr1 = ["b", "a", "c"]
get_sorted_values(arr=arr1)

arr2 = [3, 1, 2]
get_sorted_values(arr=arr2)

예를 들어 위와 같은 코드가 있다고 가정해보자. 여기서 get_sorted_arr() 함수의 arr 인자의 타입은 어떻게 명시해줘야 할까?

from typing import List, Any

def get_sorted_arr(arr: List[Any]) -> List[Any]:
    return sorted(arr)


arr1 = ["b", "a", "c"]
get_sorted_arr(arr=arr1)

arr2 = [3, 1, 2]
get_sorted_arr(arr=arr2)

arr1은 str 타입으로 구성된 리스트이고 arr2는 int타입으로 구성된 리스트이기 때문에 위처럼 List[Any]로 명시를 해줘야한다. 여기서 제네릭을 통해 좀 더 명확하게 명시해줄 수 있다.

from typing import List, Generic, TypeVar


T = TypeVar("T")

def get_sorted_arr(arr: List[T]) -> List[T]:
    return sorted(arr)


arr1: List[str] = ["b", "a", "c"]
get_sorted_arr(arr=arr1)

arr2: List[int] = [3, 1, 2]
get_sorted_arr(arr=arr2)

먼저 TypeVar를 통해 제네릭 타입을 하나 정의한다. 그리고 arr의 타입을 List[T]로 정의한다. 일반적으로 자바 등의 언어를 사용해본 사람들은 제네릭에 익숙할텐데, 간단하게 설명하자면 제네릭은 외부에서 타입을 지정해주는 것이라고 생각하면 된다. 따라서 실제 사용하는 시점에 타입을 명시하여 사용할 수 있기 때문에 코드의 재사용성을 높여준다. 

위 코드에서는 T라는 제네릭 타입을 생성하였기에 실제 arr1(List[str])과 arr2(List[int])를 통해 get_sorted_arr() 함수가 호출될 때 내부적으로 각 리스트의 타입이 들어온다고 생각한다고 보면 된다.

T = TypeVar("T")

def get_sorted_arr(arr: List[T]) -> List[T]:
    temp = sorted(arr)
    return 1


arr1: List[str] = ["b", "a", "c"]
get_sorted_arr(arr=arr1)

예를 들어 위처럼 List[T]를 받고 List[T]를 받는데, 1이라는 int를 리턴하는 코드가 있을 때 mypy를 사용하여 정상적인 타입이 맞는지 체크해줄 수 있다.

TypeVar

T = TypeVar("T", str, int)

TypeVar에는 추가적인 인자도 입력할 수 있는데 위와 같이 원하는 타입을 입력함으로써 해당 제네릭 타입은 str 또는 int라는 제한을 둘 수 있다.

T = TypeVar("T", bound=int)

또는 위처럼 bound인자에 원하는 타입을 입력하여 해당 타입 또는 해당 타입을 상속받은 타입이라고 명시해줄 수 있다. 위 예제는 int 또는 int를 상속받은 타입이라는 걸 나타낸다.

from typing import Generic, Dict, TypeVar


T = TypeVar("T")


class Container(Generic[T]):
    def __init__(self):
        self.storage: Dict[str, T] = {}

    def set(self, k: str, v: T) -> None:
        self.storage[k] = v

    def get(self, k: str) -> T:
        return self.storage[k]
        
        
c1 = Container[str]()
c2 = Container[int]()

위처럼 클래스에도 사용할 수 있다. 클래스를 작성할 때 내부적으로 사용할 제네릭 타입을 Generic으로 감싸서 상속받는 형태로 구현한다. 그리고 해당 클래스를 통해 실제 인스턴스를 생성할 때는 가장 아래 라인에 있는 c1, c2처럼 타입을 명시하여 생성한다. 이런 방식을 사용하면 클래스 내부에서 사용되는 T변수는 제네릭 변수이기 때문에 외부에서 인스턴스를 생성할 때 명시해준 타입이라고 가정하게 된다.

Reference

https://medium.com/@steveYeah/using-generics-in-python-99010e5056eb