在创建后向 python 枚举添加一个 mixin?

Add a mixin to python enum after it's created?

问题

假设我定义了一个枚举:

from enum import Enum, auto

class Foo(Enum):
   DAVE_GROHL = auto()
   MR_T = auto()

我想用特定的方法扩展这个class(即,没有新的枚举成员):

class MixinFoo(Foo):
   def catch_phrase(self):
      if self is Foo.MR_T:
         return "I pity da foo!"
      else:
         return "There goes my hero!"

这将因 TypeError 而失败,因为根据设计,一旦定义了成员,就无法扩展枚举。 Python enum 允许您在另一个方向上执行此操作(即:定义没有成员的 mixin 和 subclass mixin 以添加成员),但在我的特定用例中,定义 MixinFooFoo 之前用作基础 class 不是一个选项(例如:Foo 是在另一个模块中定义的,我不想重新创建它或修改该模块的代码)。

到目前为止我考虑的内容:

调整 EnumMetaEnumMeta__prepare__ 方法中检查基础 class 中的现有成员,因此覆盖此方法并推迟在新元 class 中进行检查可能会起作用。不幸的是,EnumMeta 检查其他几个函数中的现有成员,并且重新定义所有这些函数通常似乎是不好的做法(很多重复的代码)。

事后添加功能。我知道可以这样做:

def catch_phrase(self):
   ...

Foo.catch_phrase = catch_phrase

虽然这可能有效,但我想避免一些明显的缺点(例如:对于大量函数来说非常麻烦,static/class 方法,正常 class 定义的属性可能难以实施)

解决组合优于继承的问题:

class FooWrapper():
   def __init__(self, foo):
      self._foo = foo

   def catch_phrase(self):
      ...

虽然这是可能的,但我不是这种方法的忠实粉丝。

一些相关问题:

为什么 EnumMeta__prepare__ 以外的现有成员进行检查?从设计的角度来看,这似乎不仅是多余的,而且还使 EnumMeta 的扩展变得不必要地困难,就像我的情况一样。我理解不允许使用更多成员扩展枚举背后的基本原理,但是在枚举库的设计中显然考虑了混合。这是对枚举设计的疏忽吗?是否考虑并拒绝了?出于某种原因是故意的吗?

此外,对现有成员的一项检查隐藏在 _EnumDict class 中,这是不可访问的,所以我不确定如果没有重新创建相当大一部分的枚举库。

总体意图是我有一个共同的枚举,其中成员是已知和固定的,但用例和方法是特定于应用程序的(即,使用 Foo 的不同应用程序会有不同的问候语,并且可能要添加的其他功能,但没有应用程序需要添加其他 Foos).

选项 1:将新方法分配给现有枚举

简单地将方法分配给 Foo 是否可行?

喜欢Foo.catch = lambda self=None: print(Foo if self is None else "MR_T" if self is Foo.MR_T else "Ka-bong")

如果不是,为什么?对于所有目的,它的行为确实与方法一样,如果从成员调用该方法,则“self”将填充“Foo”成员。

如果您想受益于 Python 创建 classes 的机制和“class”语句块语法,使用“伪”元很容易实现class:一个可调用对象,它将包含 class 主体的内容,以及名称和基数——它们可以将新创建​​的方法和属性添加到现有枚举 class :

def EnumMixinExtender(name, bases, ns):
    original = bases[0]
    for k, v in ns.items():
        setattr(original, k, v)
    return original

这将允许您使用

扩展 Foo
class MixinFoo(Foo, metaclass=EnumMixinExtender):
   def catch_phrase(self=None):
        ...

有了这个选项,MixinFoo is FooMixinFoo.member1 is Foo.member1:Foo class 被猴子修补到位 - 无需关心导入顺序:每个使用“Foo”的人都可以使用“ catch_phrase".

选项 2 - MixinFoo != Foo:

如果必须保留不同的 Foo class 并且需要 Foo.member1 is not MixinFoo.member1 断言 True - 出路,仍然使用 pseudo-metaclass 是颠倒事物,并且创建一个 Foo 的副本,它将派生自 Mixin 中定义的新 class-members。 metaclass 代码只是以相反的顺序执行此操作,因此 Enum class 稍后进入组合。

def ReverseMixinAdder(name, bases, ns):
    pre_bases = tuple(base for base in bases if not isinstance(base, enum.EnumMeta))
    pos_bases = tuple(base for base in bases if isinstance(base, enum.EnumMeta))

    new_mixin = type(f"{name}_mixin", pre_bases, ns)
    member_space = {}
    for base in pos_bases:
        member_space |= {k: v.value for k, v in base.__members__.items()}
    new_enum = Enum(name, member_space, type=new_mixin )
    return new_enum

只需将其用作 metaclas,就像上面那样,您将获得一个独立的“MixinFoo”class,它可以独立于 Foo 进行传递和使用。缺点是不仅它的成员“不是” Foo 成员,只是副本,而且 MixinFoo 不会是子class 或与 Foo 根本不相关。

(顺便说一句,如果您不使用它们,请忽略允许一个合并多个枚举的机制。我也没有测试过那部分)

选项 3 - MixinFoo != Foo,但是 issubclass(MixinFoo, Foo):

如果必须将 MixinFoo 作为不同于 Foo 的 class 并且仍然有 MixinFoo.member1 is Foo.member1issubclass(MixinFoo, Foo) 断言为真,

好吧...在尝试使用混合方法扩展 Enum 时查看回溯:

    565                 if issubclass(base, Enum) and base._member_names_:
--> 566                     raise TypeError(

当然这是一个实现细节 - 但此时所有 EnumMeta 检查都是...... member_names - 这恰好是可分配的。

如果简单地为“Foo.member_names”分配一个假值,它可以被子classed,使用额外的混合方法和普通属性随意。 ._member_names_之后可以恢复

这样做,这些断言成立:FooMixin.member1 is Foo.member1FooMixin is not Fooissubclass(FooMixin, Foo)。然而,新方法不能直接在成员上调用——也就是说,在前两个选项中可以做 FooMixin.member1.catch_phrase(),而在这种情况下,成员枚举保持 class Foo 没有新方法,那是行不通的。 (解决方法是 FooMixin.catch_phrase(FooMixin.member1)。

如果希望这最后一部分起作用,Foo.members 可以将其 __class__ 属性更新为 FooMixin - 它会起作用,但原始的 Foo.members也已就地更新。

我有直觉,这个最终形式就是你在这里真正要求的 -

class MetaMixinEnum(enum.EnumMeta):
    registry = {}

    @classmethod
    def _get_enum(mcls, bases):
        enum_index, member_names = next((i, base._member_names_) for i, base in enumerate(bases) if issubclass(base, Enum))
        return bases[enum_index], member_names

    @classmethod
    def __prepare__(mcls, name, bases):
        base_enum, member_names = mcls._get_enum(bases)
        mcls.registry[base_enum] = member_names
        base_enum._member_names_ = []
        return super().__prepare__(name, bases)

    def __new__(mcls, name, bases, ns):
        base_enum, _  = mcls._get_enum(bases)
        try:
            new_cls = super().__new__(mcls, name, bases, ns)
        finally:
            member_names = base_enum._member_names_ = mcls.registry[base_enum]
        new_cls._member_names_ = member_names[:]
        for name in member_names:
            setattr(getattr(new_cls, name), "__class__", new_cls)
        return new_cls


class FooMixin(Foo, metaclass=MetaMixinEnum):
    def catch_phrase(self):
        ...

选项 4 - issubclass(Foo, MixinFoo)

以@jsbueno 为基础,灵感来自@l4mpi 在this question 中的回答。在这里,我们 monkeypatch Foo 使其成为 MixinFoo 的子类,就像典型的 Enum mixin 的情况一样。我们只需将 MixinFoo 添加到 Foo 的基数:

class Foo(Enum):
   ...

class MixinFoo(Enum):
   def catch_phrase(self):
      ...

Foo.__bases__ = (MixinFoo, ) + Foo.__bases__

for foo in Foo:
   foo.catch_phrase()

Why does EnumMeta have checks for existing members outside of __prepare__?

好问题。它不再这样做(从 3.11 开始)。

Furthermore, one of the checks for existing members is buried in the _EnumDict class

否,_EnumDict 中的检查是针对已在该枚举 中定义的成员 :

class Foo(Enum):
    BAR = 1
    BAR = 1 # this errors out because of _EnumDict

我喜欢你的选项 4 -- 当然,它应该是 decorator:

def add_methods_to(cls):
    def insert_class(scls):
        cls.__bases__ = (scls, ) + cls.__bases__
        return scls
    return insert_class

并在行动:

@add_methods_to(Foo)
class MixinFoo:             # mix-in class does not need to be an enum
    def catch_phrase(self):
        return 'blah blah'