英文:
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
视为子类型的能力
有一些功能性的解决方案,但我提出这个问题是为了找到一个“最佳方式”而不仅仅是一个“可行方式”。
以下是我找到的内容:
-
最简单的解决方法是使用
Literal
:SomeGenericType[Literal[Dim.TIME]]
,但这种方式每次都写很麻烦,而且对于希望Dim.TIME
表现为类型的人来说有点反直觉。 -
转向类,最直观的想法:
class Dimension:
pass
class TIME(Dimension):
pass
不起作用,因为我希望type(TIME)
是Dim
,以重现Enum的行为。
- 这导致使用元类:
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
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, andDim.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:
-
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. -
Switching to classes, the most intuitive idea:
class Dimension: pass class TIME(Dimension): pass
doesn't work, because I want
type(TIME)
to beDim
, to reproduce Enum behavior -
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 getDim.TIME
fromDim('t')
, ...
答案1
得分: 2
> 有没有像Enum一样简单的构造,但是可以创建类型而不是值?
是的,元类(metaclass)。元类用于创建类型。就使用方面来说,它很简单,即创建新类型,但是您需要投入一些额外的工作来正确设置它。
从语义上讲,您可以将Dimension
视为一种类型,Time
、Distance
等视为其实例。换句话说,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("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
And to create new Dimension
classes you just inherit from 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
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论