gpt4 book ai didi

python - 如何使用别名扩展枚举

转载 作者:行者123 更新时间:2023-12-04 08:24:47 27 4
gpt4 key购买 nike

我有一个抽象基类 GameNodeState,它包含一个 Type 枚举:

import abc
import enum


class GameNodeState(metaclass=abc.ABCMeta):
class Type(enum.Enum):
INIT = enum.auto()
INTERMEDIATE = enum.auto()
END = enum.auto()

枚举中的名称是通用的,因为它们必须对 GameNodeState 的任何子类有意义。但是当我将 GameNodeState 子类化为 GameStateRoundState 时,我希望能够为 GameNodeState 的成员添加具体的别名.Type 如果通过子类访问枚举。例如,如果 GameState 子类将 INTERMEDIATE 别名为 ROUNDRoundState 别名为 INTERMEDIATE作为 TURN,我想要以下行为:

>>> GameNodeState.Type.INTERMEDIATE
<Type.INTERMEDIATE: 2>

>>> RoundState.Type.TURN
<Type.INTERMEDIATE: 2>

>>> RoundState.Type.INTERMEDIATE
<Type.INTERMEDIATE: 2>

>>> GameNodeState.Type.TURN
AttributeError: TURN

我的第一个想法是:

class GameState(GameNodeState):
class Type(GameNodeState.Type):
ROUND = GameNodeState.Type.INTERMEDIATE.value


class RoundState(GameNodeState):
class Type(GameNodeState.Type):
TURN = GameNodeState.Type.INTERMEDIATE.value

但是枚举不能被子类化。


注意:GameNodeState 层次结构中显然有更多的属性和方法,我在这里将其剥离到最低限度以专注于这个特定的东西。

最佳答案

细化

(下面是原始解决方案。)

我从上面的代码中提取了一个中间概念,即枚举联合的概念。这可用于获得上述行为,并且在其他情况下也很有用。可以找到密码here , 我问了 Code Review question .

我会在这里添加代码以供引用:

import enum
import itertools as itt
from functools import reduce
import operator
from typing import Literal, Union

import more_itertools as mitt


AUTO = object()


class UnionEnumMeta(enum.EnumMeta):
"""
The metaclass for enums which are the union of several sub-enums.

Union enums have the _subenums_ attribute which is a tuple of the enums forming the
union.
"""

@classmethod
def make_union(
mcs, *subenums: enum.EnumMeta, name: Union[str, Literal[AUTO], None] = AUTO
) -> enum.EnumMeta:
"""
Create an enum whose set of members is the union of members of several enums.

Order matters: where two members in the union have the same value, they will
be considered as aliases of each other, and the one appearing in the first
enum in the sequence will be used as the canonical members (the aliases will
be associated to this enum member).

:param subenums: Sequence of sub-enums to make a union of.
:param name: Name to use for the enum class. AUTO will result in a combination
of the names of all subenums, None will result in "UnionEnum".
:return: An enum class which is the union of the given subenums.
"""
subenums = mcs._normalize_subenums(subenums)

class UnionEnum(enum.Enum, metaclass=mcs):
pass

union_enum = UnionEnum
union_enum._subenums_ = subenums

if duplicate_names := reduce(
set.intersection, (set(subenum.__members__) for subenum in subenums)
):
raise ValueError(
f"Found duplicate member names in enum union: {duplicate_names}"
)

# If aliases are defined, the canonical member will be the one that appears
# first in the sequence of subenums.
# dict union keeps last key so we have to do it in reverse:
union_enum._value2member_map_ = value2member_map = reduce(
operator.or_, (subenum._value2member_map_ for subenum in reversed(subenums))
)
# union of the _member_map_'s but using the canonical member always:
union_enum._member_map_ = member_map = {
name: value2member_map[member.value]
for name, member in itt.chain.from_iterable(
subenum._member_map_.items() for subenum in subenums
)
}
# only include canonical aliases in _member_names_
union_enum._member_names_ = list(
mitt.unique_everseen(
itt.chain.from_iterable(subenum._member_names_ for subenum in subenums),
key=member_map.__getitem__,
)
)

if name is AUTO:
name = (
"".join(subenum.__name__.removesuffix("Enum") for subenum in subenums)
+ "UnionEnum"
)
UnionEnum.__name__ = name
elif name is not None:
UnionEnum.__name__ = name

return union_enum

def __repr__(cls):
return f"<union of {', '.join(map(str, cls._subenums_))}>"

def __instancecheck__(cls, instance):
return any(isinstance(instance, subenum) for subenum in cls._subenums_)

@classmethod
def _normalize_subenums(mcs, subenums):
"""Remove duplicate subenums and flatten nested unions"""
# we will need to collapse at most one level of nesting, with the inductive
# hypothesis that any previous unions are already flat
subenums = mitt.collapse(
(e._subenums_ if isinstance(e, mcs) else e for e in subenums),
base_type=enum.EnumMeta,
)
subenums = mitt.unique_everseen(subenums)
return tuple(subenums)


def enum_union(*enums, **kwargs):
return UnionEnumMeta.make_union(*enums, **kwargs)

一旦我们有了它,我们就可以定义 extend_enum 装饰器来计算基本枚举和枚举“扩展”的联合,这将导致所需的行为:

def extend_enum(base_enum):
def decorator(extension_enum):
return enum_union(base_enum, extension_enum)

return decorator

用法:

class GameNodeState(metaclass=abc.ABCMeta):
class Type(enum.Enum):
INIT = enum.auto()
INTERMEDIATE = enum.auto()
END = enum.auto()


class RoundState(GameNodeState):
@extend_enum(GameNodeState.Type)
class Type(enum.Enum):
TURN = GameNodeState.Type.INTERMEDIATE.value


class GameState(GameNodeState):
@extend_enum(GameNodeState.Type)
class Type(enum.Enum):
ROUND = GameNodeState.Type.INTERMEDIATE.value

现在上面的所有示例都产生相同的输出(加上添加的实例检查,即 isinstance(RoundState.Type.TURN, RoundState.Type) 返回 True) .

我认为这是一个更简洁的解决方案,因为它不涉及处理描述符;它不需要了解有关所有者类的任何信息(这与顶级类一样有效)。

通过 GameNodeState 的子类和实例进行的属性查找应该自动链接到正确的“扩展”(即联合),只要添加的扩展枚举与 的名称相同>GameNodeState 父类(super class),以便它隐藏原始定义。


原创

不确定这个想法有多糟糕,但这里有一个解决方案,它使用环绕枚举的描述符,该枚举基于访问它的类获取别名集。

class ExtensibleClassEnum:
class ExtensionWrapperMeta(enum.EnumMeta):
@classmethod
def __prepare__(mcs, name, bases):
# noinspection PyTypeChecker
classdict: enum._EnumDict = super().__prepare__(name, bases)
classdict["_ignore_"] = ["base_descriptor", "extension_enum"]
return classdict

# noinspection PyProtectedMember
def __new__(mcs, cls, bases, classdict):
base_descriptor = classdict.pop("base_descriptor")
extension_enum = classdict.pop("extension_enum")
wrapper_enum = super().__new__(mcs, cls, bases, classdict)
wrapper_enum.base_descriptor = base_descriptor
wrapper_enum.extension_enum = extension_enum

base, extension = base_descriptor.base_enum, extension_enum
if set(base._member_map_.keys()) & set(extension._member_map_.keys()):
raise ValueError("Found duplicate names in extension")
# dict union keeps last key so we have to do it in reverse:
wrapper_enum._value2member_map_ = (
extension._value2member_map_ | base._value2member_map_
)
# union of both _member_map_'s but using the canonical member always:
wrapper_enum._member_map_ = {
name: wrapper_enum._value2member_map_[member.value]
for name, member in itertools.chain(
base._member_map_.items(), extension._member_map_.items()
)
}
# aliases shouldn't appear in _member_names_
wrapper_enum._member_names_ = list(
m.name for m in wrapper_enum._value2member_map_.values()
)
return wrapper_enum

def __repr__(self):
# have to use vars() to avoid triggering the descriptor
base_descriptor = vars(self)["base_descriptor"]
return (
f"<extension wrapper enum for {base_descriptor.base_enum}"
f" in {base_descriptor._extension2owner[self]}>"
)

def __init__(self, base_enum):
if not issubclass(base_enum, enum.Enum):
raise TypeError(base_enum)
self.base_enum = base_enum
# The user won't be able to retrieve the descriptor object itself, just
# the enum, so we have to forward calls to register_extension:
self.base_enum.register_extension = staticmethod(self.register_extension)

# mapping of owner class -> extension for subclasses that define an extension
self._extensions: Dict[Type, ExtensibleClassEnum.ExtensionWrapperMeta] = {}
# reverse mapping
self._extension2owner: Dict[ExtensibleClassEnum.ExtensionWrapperMeta, Type] = {}

# add the base enum as the base extension via __set_name__:
self._pending_extension = base_enum

@property
def base_owner(self):
# will be initialised after __set_name__ is called with base owner
return self._extension2owner[self.base_enum]

def __set_name__(self, owner, name):
# step 2 of register_extension: determine the class that defined it
self._extensions[owner] = self._pending_extension
self._extension2owner[self._pending_extension] = owner
del self._pending_extension

def __get__(self, instance, owner):
# Only compute extensions once:
if owner in self._extensions:
return self._extensions[owner]

# traverse in MRO until we find the closest supertype defining an extension
for supertype in owner.__mro__:
if supertype in self._extensions:
extension = self._extensions[supertype]
break
else:
raise TypeError(f"{owner} is not a subclass of {self.base_owner}")

# Cache the result
self._extensions[owner] = extension
return extension

def make_extension(self, extension: enum.EnumMeta):
class ExtensionWrapperEnum(
enum.Enum, metaclass=ExtensibleClassEnum.ExtensionWrapperMeta
):
base_descriptor = self
extension_enum = extension

return ExtensionWrapperEnum

def register_extension(self, extension_enum):
"""Decorator for enum extensions"""
# need a way to determine owner class
# add a temporary attribute that we will use when __set_name__ is called:
if hasattr(self, "_pending_extension"):
# __set_name__ not called after the previous call to register_extension
raise RuntimeError(
"An extension was created outside of a class definition",
self._pending_extension,
)
self._pending_extension = self.make_extension(extension_enum)
return self

用法如下:

class GameNodeState(metaclass=abc.ABCMeta):
@ExtensibleClassEnum
class Type(enum.Enum):
INIT = enum.auto()
INTERMEDIATE = enum.auto()
END = enum.auto()


class RoundState(GameNodeState):
@GameNodeState.Type.register_extension
class Type(enum.Enum):
TURN = GameNodeState.Type.INTERMEDIATE.value


class GameState(GameNodeState):
@GameNodeState.Type.register_extension
class Type(enum.Enum):
ROUND = GameNodeState.Type.INTERMEDIATE.value

然后:

>>> (RoundState.Type.TURN 
... == RoundState.Type.INTERMEDIATE
... == GameNodeState.Type.INTERMEDIATE
... == GameState.Type.INTERMEDIATE
... == GameState.Type.ROUND)
...
True

>>> RoundState.Type.__members__
mappingproxy({'INIT': <Type.INIT: 1>,
'INTERMEDIATE': <Type.INTERMEDIATE: 2>,
'END': <Type.END: 3>,
'TURN': <Type.INTERMEDIATE: 2>})

>>> list(RoundState.Type)
[<Type.INTERMEDIATE: 2>, <Type.INIT: 1>, <Type.END: 3>]

>>> GameNodeState.Type.TURN
Traceback (most recent call last):
...
File "C:\Program Files\Python39\lib\enum.py", line 352, in __getattr__
raise AttributeError(name) from None
AttributeError: TURN

关于python - 如何使用别名扩展枚举,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65321592/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com