将Python方法的通用参数默认分配给类的通用参数

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

Assign Python method generic argument to class generic argument by default

问题

假设我想要一个返回通用类型的方法,并且我希望它默认为类的参数,但仍然可以在函数调用时进行覆盖,就像这样:

T = TypeVar('T', bound=str)
U = TypeVar('U')

class Foo(Generic[T]):
    def bar(self, response_type: Type[U] = T) -> U:
        pass

这样你就可以这样使用:

x: Foo[int] = ...
x.bar() # 返回 int
x.bar(response_type=str) # 返回 str

Python 的类型检查器是否支持这种类型系统?

此外,我还希望 Foo 默认为 str,这样:

x = Foo(...)
x.bar() # 默认返回 str,即使在构造 Foo 时未指定

bound 参数是否可以实现这一点?

英文:

Say I want a method to return a generic type and I would like it to default to the a argument of the class by default but to still be overridable at function call, like this:

T = TypeVar('T', bound=str)
U = TypeVar('U')

class Foo(Generic[T]):
    def bar(self, response_type: Type[U] = T) -> U:
        pass

So you could have:

x: Foo[int] = ...
x.bar() # Returns int
x.bar(response_type=str) # Returns str

Does any Python type checker type system support this?

Additionally, I would also like Foo to default to str, so that:

x = Foo(...)
x.bar() # Returns str by default even if unspecified when constructing Foo

Does the bound parameter enable this?

答案1

得分: 1

从纯粹的键入角度来看,可以使用overload来表达你想要的内容,就像这样:

from typing import Generic, Optional, TypeVar, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        ...  # 实现


x: Foo[int]
reveal_type(x.bar())                   # 注意:显示的类型是 "builtins.int"
reveal_type(x.bar(response_type=str))  # 注意:显示的类型是 "builtins.str"

使用Mypy的reveal_type可以显示类型检查器正确推断的返回类型。


真正的挑战可能在于实际的实现,因为除非通过构造函数显式传递类型参数,否则你将无法将类型参数传递给Foo,除非你通过构造函数显式传递它。

当然,如果你将其作为强制性的__init__参数,那么就很容易了。然后你可以像下面这样做:

from typing import Generic, Optional, TypeVar, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    def __init__(self, type_arg: type[T]) -> None:
        self.type_arg = type_arg

    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        if response_type is None:
            ...
            return self.type_arg()
        ...
        return response_type()


x = Foo(int)
i = x.bar()
s = x.bar(response_type=str)

print(repr(i))  # 0
print(repr(s))  # ''

显然,具体的细节取决于你的实际用例,但你可以看到这个概念。


但是,如果你想仅依赖于通过__class_getitem__传递类型参数,你就需要有创意了。没有内置的方法可以使方法参数的默认值自动成为传递给__class_getitem__的类型参数。

泛型别名类型的构造方式是,在初始化期间完全丢失类型参数,这意味着当你初始化Foo[int]时,它看起来与初始化任何其他Foo[T]或仅仅是Foo一样。从文档中可以看到:

参数化泛型在对象创建期间擦除类型参数

现在,在初始化之后,会添加__orig_class__属性,它指向使用的泛型别名类型,但这对于__init__方法没有帮助。

我猜你可以推迟检查__orig_class__,直到调用相关的bar方法。因此,解决这个问题的一种可能方法如下:

from typing import Generic, Optional, TypeVar, get_args, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        if response_type is not None:
            ...
            return response_type()
        orig_class = getattr(self, "__orig_class__", None)
        if orig_class is None:
            raise TypeError("No type parameter available!")
        type_arg = get_args(orig_class)[0]
        ...
        return type_arg()


x = Foo[int]()
i = x.bar()
s = x.bar(response_type=str)

print(repr(i))  # 0
print(repr(s))  # ''

但正如@user2357112在评论中指出的那样,这不是一个非常健壮的方法。它没有解决子类化的问题,即class Bar(Foo[int]): ...将不保留__orig_class__。你可以再次解决这个问题,例如在__init_subclass__中获取__orig_bases__,但这将变得越来越复杂。

总的来说,你应该记住,许多类型构造在设计上几乎没有或没有运行时的影响。因此,试图从类型推导到运行时总是会很困难。

英文:

Purely from a typing perspective, it is possible to express what you want using overloads like this:

from typing import Generic, Optional, TypeVar, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        ...  # implementation


x: Foo[int]
reveal_type(x.bar())                   # note: Revealed type is "builtins.int"
reveal_type(x.bar(response_type=str))  # note: Revealed type is "builtins.str"

Using reveal_type with Mypy shows that the type checker correctly infers the return types this way.


The real challenge here could be the actual implementation because without some serious overhead you will not be able to get that type argument passed to Foo, unless you pass it in explicitly via the constructor for example.

It would be easy of course, if you had it as a mandatory __init__ argument. Then you could do something like the following:

from typing import Generic, Optional, TypeVar, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    def __init__(self, type_arg: type[T]) -> None:
        self.type_arg = type_arg

    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        if response_type is None:
            ...
            return self.type_arg()
        ...
        return response_type()


x = Foo(int)
i = x.bar()
s = x.bar(response_type=str)

print(repr(i))  # 0
print(repr(s))  # ''

Obviously the details depend on your actual use case, but you can see the concept.


But if you want to rely on passing the type argument via __class_getitem__ only, you will need to get creative. There is no built-in way to have the default for a method parameter automatically be the type argument passed to __class_getitem__.

The way generic alias types are constructed, the type argument is completely lost during initialization, meaning when you initialize Foo[int] it looks the same as if you were initializing any other Foo[T] or just Foo for that matter. From the docs:

> parameterized generics erase type parameters during object creation

Now after initialization, the __orig_class__ attribute is added, which points to the generic alias type that was used, but this does not help you in the __init__ method.

I guess you could defer checking for __orig_class__ until you call that specific bar method in question. So one possible way around this issue could look something like this:

from typing import Generic, Optional, TypeVar, get_args, overload


T = TypeVar("T")
U = TypeVar("U")


class Foo(Generic[T]):
    @overload
    def bar(self, response_type: type[U]) -> U: ...

    @overload
    def bar(self, response_type: None = None) -> T: ...

    def bar(self, response_type: Optional[type] = None) -> object:
        if response_type is not None:
            ...
            return response_type()
        orig_class = getattr(self, "__orig_class__", None)
        if orig_class is None:
            raise TypeError("No type parameter available!")
        type_arg = get_args(orig_class)[0]
        ...
        return type_arg()


x = Foo[int]()
i = x.bar()
s = x.bar(response_type=str)

print(repr(i))  # 0
print(repr(s))  # ''

But as pointed out in the comments by @user2357112, this is not a very robust way of doing it. It does not address subclassing, i.e. class Bar(Foo[int]): ... will not retain __orig_class__. You could again work around that and grab the __orig_bases__ for example in __init_subclass__, but this will get more and more hacky.

In general, you should remember that many typing constructs have minimal or no runtime implications by design. So trying to work from typing to runtime is always going to be an uphill battle.

huangapple
  • 本文由 发表于 2023年8月8日 22:14:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/76860418.html
匿名

发表评论

匿名网友

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

确定