如何测试`__annotations__`成员的类型与泛型类参数相同?

huangapple go评论158阅读模式
英文:

How to test __annotations__ member has same type as generic class parameter?

问题

我想能够只选择已注释的成员,这些成员与基类中泛型参数的类型相同:

from typing import Generic, TypeVar

T = TypeVar("T")

class MarkFinder(Generic[T]):
    def __init_subclass__(cls, **kwargs):
        cls.marked = tuple(
            name for name, annotated_type in cls.__annotations__.items() 
            if some_condition(annotated_type)
        )

这样,如果我继承:

T2 = TypeVar("T2")

class Inheritor(MarkFinder[T2]):
    a: T2
    b: int
    c: T2

那么 Inheritor.marked 应该是 ('a', 'c')

我尝试用以下替换 some_condition(annotated_type)

cls.__parameters__[0] is annotated_typecls.__parameters__[0] == annotated_type

但即使它们具有相同的名称,类型也不相等。

什么是正确的条件?或者这是不可能的?

英文:

I want to be able to select only annotated members that have the same type as the parameter of generic in this base class:

from typing import Generic, TypeVar


T = TypeVar("T")

class MarkFinder(Generic[T]):
    def __init_subclass__(cls, **kwargs):
        cls.marked = tuple(
            name for name, annotated_type in cls.__annotations__.items() 
            if some_condition(annotated_type)
        )

So that if I inherit:

T2 = TypeVar("T2")

class Inheritor(MarkFinder[T2]):
    a: T2
    b: int
    c: T2

Then Inheritor.marked should just be ('a', 'c').

I have tried to replace some_condition(annotated_type) with:

cls.__parameters__[0] is annotated_type or cls.__parameters__[0] == annotated_type

but even though they have the same names, the types are not equal.

What is the correct condition? Or is this impossible?

答案1

得分: 1

这是可能的,但有一些注意事项。该方法的关键部分以及一些需要记住的事项在以下帖子中有解释,因此建议您首先阅读它:

https://stackoverflow.com/questions/73746553

TL;DR 是从__orig_bases__中获取来自_original_基类的类型参数(它将是泛型别名类型),然后与注释进行比较(以确定其是否相同)。

根据您提出问题的方式,我假设您只想将此应用于类型变量,而不是_特定的_类型参数。以下是您可以执行此操作的方式:

from typing import Any, ClassVar, Generic, TypeVar, get_args, get_origin


T = TypeVar("T")


class MarkFinder(Generic[T]):
    marked: ClassVar[tuple[str, ...]] = ()

    @classmethod
    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        for base in cls.__orig_bases__:  # type: ignore[attr-defined]
            origin = get_origin(base)
            if origin is None or not issubclass(origin, MarkFinder):
                continue
            type_arg = get_args(base)[0]
            if not isinstance(type_arg, TypeVar):
                return  # do not touch non-generic subclasses
            cls.marked += tuple(
                name for name, annotation in cls.__annotations__.items()
                if annotation is type_arg
            )

用法演示:

T2 = TypeVar("T2")


class Child(MarkFinder[T2]):
    a: T2
    b: int
    c: T2


T3 = TypeVar("T3")


class GrandChild(Child[T3]):
    d: T3
    e: str


class SpecificDescendant1(GrandChild[int]):
    f: int


class SpecificDescendant2(GrandChild[str]):
    f: float


print(Child.marked)                # ('a', 'c')
print(GrandChild.marked)           # ('a', 'c', 'd')
print(SpecificDescendant1.marked)  # ('a', 'c', 'd')
print(SpecificDescendant2.marked)  # ('a', 'c', 'd')

检查type_arg不是TypeVar实例的部分很重要。如果没有这个检查,那么SpecificDescendant1子类也会在其marked元组中包含'f'。(如果这是您想要的,请从基类的__init_subclass__方法中删除该检查。)

英文:

This is possible, but with a few caveats. The crux of the approach as well as some of the things to keep in mind are explained in the following post, so I suggest you read it first:

https://stackoverflow.com/questions/73746553

The TL;DR is to grab the the type argument from the original base class (which will be a generic alias type) from __orig_bases__ and compare the annotations against that (for identity).

From the way you phrased your question, I assume you only want this to apply to type variables and not to specific type arguments. Here is how you could do it:

from typing import Any, ClassVar, Generic, TypeVar, get_args, get_origin


T = TypeVar("T")


class MarkFinder(Generic[T]):
    marked: ClassVar[tuple[str, ...]] = ()

    @classmethod
    def __init_subclass__(cls, **kwargs: Any) -> None:
        super().__init_subclass__(**kwargs)
        for base in cls.__orig_bases__:  # type: ignore[attr-defined]
            origin = get_origin(base)
            if origin is None or not issubclass(origin, MarkFinder):
                continue
            type_arg = get_args(base)[0]
            if not isinstance(type_arg, TypeVar):
                return  # do not touch non-generic subclasses
            cls.marked += tuple(
                name for name, annotation in cls.__annotations__.items()
                if annotation is type_arg
            )

Usage demo:

T2 = TypeVar("T2")


class Child(MarkFinder[T2]):
    a: T2
    b: int
    c: T2


T3 = TypeVar("T3")


class GrandChild(Child[T3]):
    d: T3
    e: str


class SpecificDescendant1(GrandChild[int]):
    f: int


class SpecificDescendant2(GrandChild[str]):
    f: float


print(Child.marked)                # ('a', 'c')
print(GrandChild.marked)           # ('a', 'c', 'd')
print(SpecificDescendant1.marked)  # ('a', 'c', 'd')
print(SpecificDescendant2.marked)  # ('a', 'c', 'd')

The check for the type_arg not being a TypeVar instance is important. Without it, that SpecificDescendant1 subclass would also have 'f' in its marked tuple. (If that is what you want, just remove that check from the base class' __init_subclass__ method.)

huangapple
  • 本文由 发表于 2023年8月10日 19:32:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76875332.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定