英文:
How to implement type-safe CRTP in Python?
问题
我熟悉C++和C#编程语言中的奇妙递归模板模式(CRTP)的实现。但是,在Python中如何以类型安全的方式(使用Mypy)实现相同的想法呢?
这段代码可能不会正常运行,但从符号上讲,它提供了一个想法:
class GenericParent(Generic[T]):
pass
class Derived(GenericParent[Derived]):
pass
我尝试通过Derived
类来特化GenericParent
类型变量,但是Mypy
会报错。
英文:
I'm familiar with curiously recurring template pattern (CRTP) implementation in C++ and C# programming languages. But, how we can achieve the same idea in the Python and type-safe (using Mypy)?
This code may not be functioning properly, but symbolically, it provides an idea:
class GenericParent(Generic[T]):
pass
class Derived(GenericParent[Derived]):
pass
I've tried specializing the GenericParent
type variables by the Derived
class but the Mypy
gives error.
答案1
得分: 0
作为一种静态类型模式,CRTP 应该在主要的 Python 类型检查实现中默认支持。当你声明 class Derived(GenericParent[Derived]): ...
时,mypy 本身不应该报错(参见以下的 mypy playground 演示) - 你可能会在运行时看到错误,因为这段代码会在运行时失败。
你有几种选项可以使这个工作。选项 1 和 2 假设你不需要运行时反射;选项 3 将以有限的方式处理运行时反射。
- 写一个
.pyi
存根文件,它没有运行时实现,因此不会引起运行时错误。CRTP 模式在 Python 自己的 typeshed 中用于静态建模内置的str
类型。 - 将类型信息放在
if typing.TYPE_CHECKING
下,与from __future__ import annotations
一起使用,这(依我看来)是处理前向引用和循环导入的最干净的方式。 - 使用字符串字面值来声明类型,可以直接声明或使用显式类型别名:
import typing as t T = t.TypeVar("T") class GenericParent(Generic[T]): pass class Derived(GenericParent("Derived")): pass
typing
中的一些辅助函数可以帮助执行这种内省。在 Python 3.10 中:>>> import typing as t >>> t._eval_type(Derived.__orig_bases__[0], globals(), {}) __main__.GenericParent[__main__.Derived]
英文:
As a static typing pattern, CRTP should be supported out of the box across major Python type checking implementations. mypy itself should not give you errors when you declare class Derived(GenericParent[Derived]): ...
(see the following mypy playground demo) - you might be seeing errors from another linter or the IDE you're using, because this code will fail at runtime.
You have several options to make this work. Options 1 and 2 assumes that you don't need runtime introspection; option 3 will handle runtime introspection in a limited fashion.
- Write a
.pyi
stub file, which has no runtime implementation and thus won't cause any runtime errors. The CRTP pattern is used in Python's own typeshed to statically model the builtinstr
type. - Put the typing information under
if typing.TYPE_CHECKING
along withfrom __future__ import annotations
, which (IMO) is the cleanest way to handle forward references and circular imports in general. - Use string literals to declare types, either directly or using explicit type aliases:
import typing as t T = t.TypeVar("T") class GenericParent(Generic[T]): pass class Derived(GenericParent["Derived"]): pass
Some helpers from
typing
can help perform introspecting this. On Python 3.10:>>> import typing as t >>> t._eval_type(Derived.__orig_bases__[0], globals(), {}) __main__.GenericParent[__main__.Derived]
答案2
得分: 0
只为类型安全性,大多数情况下,您可以使用 Self 类型变量:
在 Python 3.11 之前:
from typing import TypeVar
TShape = TypeVar("TShape", bound="Shape")
class Shape:
def scale(self: TShape, factor: float) -> TShape:
...
class Circle(Shape):
pass
reveal_type(Circle().scale(factor=2)) # 显示 "Circle"
从 Python 3.11 开始:
from typing import Self
class Shape:
def scale(self, factor: float) -> Self:
...
class Circle(Shape):
pass
reveal_type(Circle().scale(factor=2)) # 显示 "Circle"
此外,您可以通过简单地调用 self.dervied_method()
来调用 Derived
类的方法。
英文:
For only type-safety, most of the time, you can use the Self type variable:
Before Python 3.11:
from typing import TypeVar
TShape = TypeVar("TShape", bound="Shape")
class Shape:
def scale(self: TShape, factor: float) -> TShape:
...
class Circle(Shape):
pass
reveal_type(Circle().scale(factor=2)) # Reveals "Circle"
From Python 3.11 onwards:
from typing import Self
class Shape:
def scale(self, factor: float) -> Self:
...
class Circle(Shape):
pass
reveal_type(Circle().scale(factor=2)) # Reveals "Circle"
Also, you can call the Derived
class methods by simply calling self.dervied_method()
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论