使用枚举值作为类型变量,而不使用 Literal

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

Using Enum values as type variables, without using Literal

问题

我正在尝试表示物理维度(长度、时间、温度等),但无法找到一种与类型提示和泛型兼容的好方法。

理想情况下,我希望能够定义一个枚举,其名称本身就是类型(元枚举?):

from enum import Enum

class Dim(Enum):
    TIME = "t"
    MASS = "m"

我可以为维度进行类型提示(dim: Dim),但不能做这样的事情:

from typing import Generic, TypeVar

T = TypeVar("T", bound=Dim)  # 仅接受`Dim`

class PhysicalQuantity(Generic[T]):
    pass

class Container:
    some_time: PhysicalQuantity[Dim.TIME]  # 不起作用

因为这些是值

是否有一种像Enum一样简单的构造方式,但是可以生成类型而不是值?

我想保留Enum的原因有:

  • 定义非常容易
  • 非常容易与值(str)关联
  • 能够将Dim视为类型,Dim.TIME视为子类型的能力

有一些功能性的解决方案,但我提出这个问题是为了找到一个“最佳方式”而不仅仅是一个“可行方式”。

以下是我找到的内容:

  1. 最简单的解决方法是使用LiteralSomeGenericType[Literal[Dim.TIME]],但这种方式每次都写很麻烦,而且对于希望Dim.TIME表现为类型的人来说有点反直觉。

  2. 转向类,最直观的想法:

class Dimension: 
    pass

class TIME(Dimension): 
    pass

不起作用,因为我希望type(TIME)Dim,以重现Enum的行为。

  1. 这导致使用元类:
class Dimension(type):
    # ... 完成__init__和__new__以获得TIME.symbol = "t"

class TIME(metaclass=Dimension, symbol="t"): 
    pass

这样可以工作,但我失去了从Dim('t')获取Dim.TIME的能力...

英文:

I'm trying to represent physical dimensions (length, time, temperature, ...), and cannot find a nice way to do so, that is compatible with type hinting and generics.

Ideally, I want to be able to define an Enum whose names are types themselves (a metaenum ?):

from enum import Enum

class Dim(Enum):
    TIME = "t"
    MASS = "m"

I can type hint dimensions (dim: Dim) but cannot do things like

from typing import Generic, TypeVar

T = TypeVar("T", bound=Dim)  # only accepts `Dim`

class PhysicalQuantity(Generic[T]):
    pass

class Container:
    some_time: PhysicalQuantity[Dim.TIME]  # doesn't work

because these are values.

Is there a construct as simple as Enum, but to make types instead of values ?

Reasons why I want to keep Enum:

  • very easy to define
  • very easy to associate to a value (str)
  • Ability to sort of "think of Dim as the type, and Dim.TIME as a subtype"

There are functional solutions, however I'm asking this to get a "best way" more than a "working way".
Here's what I found:

  1. The simplest solution is to do use Literal: SomeGenericType[Literal[Dim.TIME]], but this is both annoying to write each time and counter-intuitive for people who expect Dim.TIME to behave as a type.

  2. Switching to classes, the most intuitive idea:

    class Dimension: 
        pass
    
    class TIME(Dimension): 
        pass
    

    doesn't work, because I want type(TIME) to be Dim, to reproduce Enum behavior

  3. That leads to using a metaclass:

    class Dimension(type):
        # ... complete __init__ and __new__ to get TIME.symbol = "t"
    
    class TIME(metaclass=Dimension, symbol="t"): 
        pass
    

    This works, but I lose the ability to do Dim.TIME, to get Dim.TIME from Dim('t'), ...

答案1

得分: 2

> 有没有像Enum一样简单的构造,但是可以创建类型而不是值?

是的,元类(metaclass)。元类用于创建类型。就使用方面来说,它很简单,即创建新类型,但是您需要投入一些额外的工作来正确设置它。

从语义上讲,您可以将Dimension视为一种类型,TimeDistance等视为其实例。换句话说,Time类的类型是Dimension。这似乎反映了您的看法,因为您说过:

> 我希望type(Time)Dim

现在,Quantity可以被视为类型Dimension的抽象基类。没有符号的东西。

Time将继承自Quantity(因此也是类型Dimension的一部分)。到目前为止,不需要泛型。

现在,您可以定义一个Container,该容器在持有的数量类型(即Dimension的实例)方面是通用的。

元类和基类可能如下所示:

from __future__ import annotations
from typing import Any, ClassVar, TypeVar, overload

T = TypeVar("T", bound=type)

class Dimension(type):
    _types_registered: ClassVar[dict[str, Dimension]] = {}

    @overload
    def __new__(mcs, o: object, /) -> Dimension: ...

    @overload
    def __new__(
        mcs: type[T],
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        /,
        **kwargs: Any,
    ) -> T: ...

    def __new__(
        mcs,
        name: Any,
        bases: Any = None,
        namespace: Any = None,
        /,
        **kwargs: Any,
    ) -> type:
        if bases is None and namespace is None:
            return mcs._types_registered[name]
        symbol = kwargs.pop("symbol", None)
        dim = super().__new__(mcs, name, bases, namespace, **kwargs)
        if symbol is not None:
            mcs._types_registered[symbol] = dim
        return dim

class Quantity(metaclass=Dimension):  # abstract base (no symbol)
    pass

要创建新的Dimension类,您只需继承自Quantity

from typing import Generic, TypeVar, reveal_type

# ... import Quantity

class Time(Quantity, symbol="t"):
    pass

DimTime = Dimension("t")
print(DimTime)        # <class '__main__.Time'>
print(type(Time))     # <class '__main__.Dimension'>
reveal_type(DimTime)  # mypy note: Revealed type is "Dimension"

Q = TypeVar("Q", bound=Quantity)

class Container(Generic[Q]):
    """generic container for `Dimension` instances (i.e. quantities)"""
    some_quantity: Q

我意识到这完全绕过了您的Enum问题,但由于您自己将问题表述为XY Problem,通过解释您的实际意图,我认为我可以尝试提出不同的方法。

英文:

> Is there a construct as simple as Enum, but to make types instead of values?

Yes, the metaclass. A metaclass makes types. It is simple in terms of usage i.e. creation of new types, but you do need to put in some more work to set it up properly.

Semantically, you could think of the Dimension is a type and Time, Distance etc. as instances of it. In other words the type of the Time class is Dimension. This seems to reflect your view since you said:

> I want type(Time) to be Dim

Now a Quantity could be considered the abstract base class of type Dimension. Something without a symbol.

Time would inherit from Quantity (thus also being of type Dimension). No generics so needed so far.

Now you can define a Container that is generic in terms of the type(s) of quantity (i.e. instances of Dimension) it holds.

The metaclass and base class could look like this:

from __future__ import annotations
from typing import Any, ClassVar, TypeVar, overload

T = TypeVar(&quot;T&quot;, bound=type)


class Dimension(type):
    _types_registered: ClassVar[dict[str, Dimension]] = {}

    @overload
    def __new__(mcs, o: object, /) -&gt; Dimension: ...

    @overload
    def __new__(
        mcs: type[T],
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        /,
        **kwargs: Any,
    ) -&gt; T: ...

    def __new__(
        mcs,
        name: Any,
        bases: Any = None,
        namespace: Any = None,
        /,
        **kwargs: Any,
    ) -&gt; type:
        if bases is None and namespace is None:
            return mcs._types_registered[name]
        symbol = kwargs.pop(&quot;symbol&quot;, None)
        dim = super().__new__(mcs, name, bases, namespace, **kwargs)
        if symbol is not None:
            mcs._types_registered[symbol] = dim
        return dim


class Quantity(metaclass=Dimension):  # abstract base (no symbol)
    pass

And to create new Dimension classes you just inherit from Quantity:

from typing import Generic, TypeVar, reveal_type

# ... import Quantity


class Time(Quantity, symbol=&quot;t&quot;):
    pass


DimTime = Dimension(&quot;t&quot;)
print(DimTime)        # &lt;class &#39;__main__.Time&#39;&gt;
print(type(Time))     # &lt;class &#39;__main__.Dimension&#39;&gt;
reveal_type(DimTime)  # mypy note: Revealed type is &quot;Dimension&quot;


Q = TypeVar(&quot;Q&quot;, bound=Quantity)


class Container(Generic[Q]):
    &quot;&quot;&quot;generic container for `Dimension` instances (i.e. quantities)&quot;&quot;&quot;
    some_quantity: Q

I realize this completely bypasses your Enum question, but since you even phrased the question as an XY Problem yourself by explaining what your actual intent was, I thought I'd give it a go and suggest a different approach.

huangapple
  • 本文由 发表于 2023年7月13日 23:15:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76680977.html
匿名

发表评论

匿名网友

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

确定