如何在Python中实现类型安全的CRTP?

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

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 将以有限的方式处理运行时反射。

  1. 写一个 .pyi 存根文件,它没有运行时实现,因此不会引起运行时错误。CRTP 模式在 Python 自己的 typeshed 中用于静态建模内置的 str 类型
  2. 将类型信息放在 if typing.TYPE_CHECKING 下,与 from __future__ import annotations 一起使用,这(依我看来)是处理前向引用和循环导入的最干净的方式。
  3. 使用字符串字面值来声明类型,可以直接声明或使用显式类型别名
    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.

  1. 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 builtin str type.
  2. Put the typing information under if typing.TYPE_CHECKING along with from __future__ import annotations, which (IMO) is the cleanest way to handle forward references and circular imports in general.
  3. 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().

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

发表评论

匿名网友

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

确定