openlang

ClassVar 안에는 왜 타입 변수를 쓸 수 없나요?

PEP-526(https://peps.python.org/pep-0526/#class-and-instance-variable-annotations)에 따르면

ClassVar 인자에는 중첩 깊이와 관계없이 타입 변수가 들어갈 수 없습니다. T가 타입 변수라면 ClassVar[T]도 ClassVar[List[Set[T]]]도 유효하지 않습니다.

이 제약은 왜 있는 건가요? PEP에는 설명이 없는데, Typing github 기록에 답이 있을지도 모르겠네요…

멤버 두 개를 가진 제네릭 베이스 클래스에 타입을 붙이다가 이 문제를 만났어요. 하나는 특정 타입의 인스턴스를 담는 인스턴스 속성이고, 다른 하나는 그 인스턴스의 타입을 담는 클래스 속성이에요:

class _TransportBase[Conn: (httpx.Client, httpx.AsyncClient), Cred: httpx.Auth](ABC):
    base_url: str
    connection: Conn
    _credential: Cred
    _credential_type: ClassVar[Type]  # intended as ClassVar[Type[Cred]]

    @property
    def credential(self) -> Cred:
        return self._credential

    @credential.setter
    @abstractmethod
    def credential(self, incoming: Any):
        if not isinstance(incoming, self._credential_type):
            self._credential = self._credential_type(incoming)
        else:
            self._credential = incoming

ClassVar가 이렇게 제한되어 있으면 이 속성이 클래스 수준이라는 것을 명시하거나, 그 타입과 _credential의 타입 사이 관계를 표현하거나 둘 중 하나를 포기해야 해요.

아직 초안이라 수정하면서 설계는 바뀔 거예요. 제안은 환영하지만 여기서 묻는 건 ClassVar에 관한 거예요.

영어에서 번역원문 보기

이어지는 대화 2

서로의 근거를 따라
  1. 고유 링크

    ClassVar는 제네릭 인자 조합마다 따로 있는 게 아니라 class 자체에 속해요.

    다른 예로 제네릭 연결 리스트를 구현한다고 생각해 보세요.

    @dataclass
    class Link[T]:
        item: T
        following: Self | None = None
    
    class Chain[T]:
        start: ClassVar[Link[T] | None] = None
    
        def __str__(self) -> str:
            owner = type(self)
            items = list[T]()
            cursor = owner.start
            while cursor:
                assert isinstance(cursor, Link)  # assist the typechecker
                items.append(cursor.item)
                cursor = cursor.following
            return f'{owner.__qualname__}({', '.join(map(repr, items))})'
    
    numbers = Chain[int]()
    words = Chain[str]()
    
    Chain[int].start = Link[int](3, Link[int](4))
    print(numbers)  # 3, 4
    Chain[str].start = Link[str]('hello', Link[str]('world'))
    print(words)  # 'hello', 'world'
    print(numbers)  # unexpectedly! 'hello', 'world'

    int 리스트와 str 리스트는 바로 그 .start를 공유해요. 하나만 존재하거든요.

    그러니 제 생각에는 ClassVar 안에 TypeVar를 쓰는 게 별 의미가 없어요.

    영어에서 번역원문 보기
  2. 고유 링크

    네, 이제 그 이유를 이해한 것 같아요.

    제 의도는 ABC 구현에서 ClassVar를 더 구체적인 타입으로 재정의하는 거였어요. ABC 자체에서는 그 속성에 실제 값이 들어갈 일이 없으니 ABC의 선언은 주로 문서 역할이었고요.

    또 혼란스러웠던 건 authlib는 httpx2를 쓰는데 제 코드는 httpx를 써서, 이름은 같아도 기술적으로는 서로 다른 타입이었다는 거예요. 그 의존성을 업데이트하고 나니 ClassVar 주석에서 타입 매개변수로 표현하려던 베이스 클래스를 직접 사용할 수 있었어요.

    영어에서 번역원문 보기

같은 질문의 다른 갈래

  1. 다른 클래스 안에 있는 클래스를 상속하려면 어떻게 하나요?

    안녕하세요, 제가 코드를 바꿀 수 없는 패키지에 다음과 같은 클래스 계층이 있어요: 편하게 쓰려고 Inner를 감싸는 래퍼를 추가하려는데, Inner가 바깥 인스턴스에 의존해서 지금까지는 바깥 클래스의 서브클래스 안에 넣는…

    답글 1최근 활동 scala
  2. Lean4의 have 바인딩과 let 바인딩 비교

    아래 두 표현식을 보니 그리고 똑같이 동작하더군요. 정리를 증명할 때도 마찬가지예요. 서로 바꿔 써도 되나요? 차이가 뭔가요?

    답글 3최근 활동 lean-4
  3. 문자열 배열을 값으로 갖는 TypeScript Map 초기화

    각 항목이 문자열 배열을 담는 Map을 만들고 있어요. 타입 선언과 초기화를 한 문장으로 같이 하려면 어떻게 하나요? 제 (단순한) 시도는 이랬어요: 그랬더니 이렇게 나와요:

    답글 2최근 활동 typescript