英文:
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 overload
s 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论