创建后,将混合蛋白添加到Python枚举中?

发布于 2025-01-25 19:44:56 字数 1643 浏览 3 评论 0原文

这个问题

假设我有一个枚举:

from enum import Enum, auto

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

我想使用特定方法(即没有新的枚举成员):

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

这将失败,因为type> type>定义成员后,无法扩展设计枚举。 Python Enum允许您在另一个方向上执行此操作(即:不使用成员定义Mixin并为Mixin子类添加成员),但是在我的特定用例中,定义mixInfoo foo foo << /代码>用作基类的选择不是一个选项(例如:foo在另一个模块中定义,我不想重新创建它或修改该模块的代码)。

到目前为止,我考虑的是:

调整enummetaenummeta检查__准备__方法中基类中现有成员的检查,因此覆盖此方法并将支票推迟到新的元口中可以工作。不幸的是,enummeta检查了其他几个功能中现有成员的检查,重新定义所有这些功能似乎是不好的实践(许多重复的代码)。

事后添加功能。我知道可以做类似的事情:

def catch_phrase(self):
   ...

Foo.catch_phrase = catch_phrase

尽管这可能起作用,但我想避免一些重要的缺点(例如:对于大量功能,静态/类方法非常麻烦,正常类定义的属性可能是难以实施)

解决继承的组成问题:

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

   def catch_phrase(self):
      ...

尽管这是可能的,但我并不是这种方法的忠实拥护者。

一些相关问题:

为什么enummeta__准备__之外的现有成员有检查?从设计的角度来看,这似乎不仅是多余的,而且还使得不必要地扩展enummeta,就像我的情况下一样。我了解不允许更多具有更多成员的枚举的背后的理由,但是在枚举库的设计中显然考虑了Mixins。这是枚举设计的监督吗?是否被考虑和拒绝?是故意出于某种原因吗?

此外,现有成员的检查之一埋在_enumdict类中,该类是无法访问的,因此我不确定如果不重新创建相当大的一部分,我想做什么枚举库。

总体意图是我有一个常见的枚举,其中成员是已知和固定的,但是用例和方法是特定于应用程序的(即,使用foo的不同应用程序会有不同的问候要添加,但是无需添加其他foo s)。

The Problem

Suppose I have an enum defined:

from enum import Enum, auto

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

and I want to extend this class with specific methods only (ie, no new enum members):

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

This will fail with TypeError, since by design enum's can't be extended once members have been defined. Python enum allows you to do this in the other direction (ie: define mixin with no members and subclass the mixin to add members), but in my specific use case, defining MixinFoo before Foo to use as a base class is not an option (eg: Foo is defined in another module and I don't want to recreate it or modify that module's code).

What I've considered so far:

Tweaking the EnumMeta. EnumMeta checks for existing members in the base class in the __prepare__ method, so overriding this method and deferring the check in a new metaclass could work. Unfortunately, EnumMeta checks for existing members in several other functions, and redefining all those functions seems like bad practice in general (lots of duplicated code).

Adding functions after the fact. I know it's possible to do something like:

def catch_phrase(self):
   ...

Foo.catch_phrase = catch_phrase

and while this might work, there are some significant disadvantages that I would like to avoid (eg: very cumbersome for a large number of functions, static/class methods, properties of normal class definition might be difficult to implement)

Solving the problem with composition over inheritance:

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

   def catch_phrase(self):
      ...

While this is possible, I'm not a huge fan of this method.

Some Related Questions:

Why does EnumMeta have checks for existing members outside of __prepare__? From a design perspective, this seems to not only be redundant, but also makes it unnecessarily difficult to extend the EnumMeta as in my case. I understand the rationale behind not allowing extension of enums with more members, but mixins were obviously considered in the design of the enum library. Was this an oversight in the design of the enum? Was it considered and rejected? Is it intentional for some reason?

Furthermore, one of the checks for existing members is buried in the _EnumDict class, which is not accessible, so I'm not sure what I'm trying to do is possible without recreating a fairly substantial portion of the enum library.

The overall intent is that I have a common enum where the members are known and fixed, but the use case and methods are application specific (ie, different applications that use Foo would have different greetings and possibly other functions to add, but no application would need to add other Foos).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

痴情 2025-02-01 19:44:56

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

确实简单地将方法分配给foo有效吗?

foo.catch = lambda self = none:print(如果self是没有其他“ mr_t”的话,如果self是foo.mr_t else't else“ ka-bong”)

如果不是,为什么?它确实像一种方法一样行事,并且如果该方法是从成员调用的,则“自我”将被“ foo”成员填充。

如果您想从Python创建类的机制和“类”语句语法中受益,那么使用“ Pseudo” Metaclass:可召唤的可呼叫,可以占用类主体的内容,以及名称和基础, - 它可以只需将新创建的方法和属性添加到现有枚举类别:

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

这将使您在

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

此选项的情况下扩展foomixInfoo.member1是foo.member1:foo类是猴子修补的 - 无需关心导入顺序:使用“ foo”的每个人都可以使用“ catch_phrase”。

选项2 -mixInfoo!= foo:

如果必须保留独特的foo类,并且需要foo.member1不是mixinfoo.member.member1 sovererts true true-伪 - 米特类是倒转事物,并创建foo的副本,该副本将从Mixin中定义的新类成员中得出。 Metaclass代码只能以相反的顺序来实现,以便枚举类稍后出现。

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

只需将其用作上面的元素,您就会获得一个独立的“ MixInfoo”类,可以将其传递并独立于FOO。缺点是,不仅其成员“不是” foo成员,只有副本,而且MixInfoo根本不会是一个子类或其他与Foo相关的子类。

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

选项3 -mixInfoo!= foo,但是IssubClass(MixInfoo,foo):

如果必须将mixInfoo作为foo的一个独特类,并且仍然具有mixInfoo.member1是foo.member1issubClass(mixinfoo,foo) soverert true,

好吧...当人们尝试使用Mixin方法扩展枚举时,我们拥有:

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

确保这是一个实现细节 - 但是此时所有的Enummeta检查都是... Member_names - 恰好是可分配的。

如果一个人简单地将虚假值分配给“ foo。 member_names ”,则可以随意通过额外的mixin方法和普通属性进行子分类。 ._ Member_names _以后可以还原。

这样做,这些断言存在:foomixin.member1是foo.member1foomixin不是foo, issubclass(foomixin,foomixin,foo)。但是,新方法不能直接在成员上调用 - 也就是说,在前两个选项中,一个可以执行foomixin.member1.catch_phrase(),因为成员枚举保持class没有新方法,不起作用。 (解决方法是foomixin.catch_phrase(foomixin.member1)。

如果有人希望这最后一部分起作用,则foo.members可以拥有其类__ class __ class __属性更新为foomixin - 它将起作用,但是原始的foo.members也已更新Intherplo

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):
        ...

Option 1: assign new methods to existing Enum

Does simply assigning a method to Foo works?

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

If not, why? It does behave, for all purposes as a method would, and "self" is filled with the "Foo" member if the method is called from the member.

If you want to benefit from Python's mechanism for creating classes and the "class" statement block syntax, this is easily feasible with a "pseudo" metaclass: a callable that will take in the contents of the class body, along with the name and bases - it can them just add the newly created methods and attributes to the existing Enum class:

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

This will allow you to extend Foo in place with

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

With this option, MixinFoo is Foo and MixinFoo.member1 is Foo.member1: the Foo class is monkey patched in place - no need to care about import order: everyone using "Foo" can use "catch_phrase".

Option 2 - MixinFoo != Foo:

If one must preserve a distinct Foo class and need that Foo.member1 is not MixinFoo.member1 asserts True - the way out, still using a pseudo-metaclass is to invert things, and create a copy of Foo that would derive from the new class-members defined in the Mixin. The metaclass code just does that in the reversed order, so that the Enum class comes later into the mix.

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

Just use that as the metaclas, like above, and you get an independent "MixinFoo" class, that can be passed around and used independent from Foo. The drawback is that not only its members "are not" Foo members, just copies, but MixinFoo won't be a subclass or otherwise related to Foo at all.

(btw, just ignore the mechanisms allowing one to merge more than one Enum if you won't use them. I had not tested that part either)

Option 3 - MixinFoo != Foo, but issubclass(MixinFoo, Foo):

If one must have MixinFoo as a distinct class from Foo and still have MixinFoo.member1 is Foo.member1 and issubclass(MixinFoo, Foo) to assert True,

Well... looking into the traceback when one tries to extend an Enum with mixin methods we have:

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

Sure this is an implementation detail - but all EnumMeta checks at this point is... member_names - which happens to be assignable.

If one simply assigns a falsey value to "Foo.member_names", it can be subclassed, with extra mixin methods and ordinary attributes at will. The ._member_names_ can be restored afterwards.

Doing this, these assertions hold: FooMixin.member1 is Foo.member1, FooMixin is not Foo, issubclass(FooMixin, Foo). However, the new methods can not be called directly on the members - that is, in the previous two options one can do FooMixin.member1.catch_phrase(), while under this, as the member enum keeps the class Foo which does not have the new method, that does not work. (The workaround is FooMixin.catch_phrase(FooMixin.member1) .

If one wants this last part to work, Foo.members can have their __class__ attribute updated to FooMixin - and it will work, but then the original Foo.members are updated inplace as well.

I have the intuition this final form is what you are really asking for here -

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):
        ...
老子叫无熙 2025-02-01 19:44:56

选项4-

@jsbueno构建的IssubClass(foo,mixinfoo),从 @l4mpi in @in 这个问题。在这里,我们是monkeypatch foo,因此它是mixInfoo的子类,就像典型的枚举混音一样。我们只需将mixInfoo添加到foo的基础:

class Foo(Enum):
   ...

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

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

for foo in Foo:
   foo.catch_phrase()

Option 4 - issubclass(Foo, MixinFoo)

Building on @jsbueno's with inspiration from an answer from @l4mpi in this question. Here we monkeypatch Foo such that it is a subclass of MixinFoo, as would be the case for a typical Enum mixin. We simply add MixinFoo to Foo's bases:

class Foo(Enum):
   ...

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

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

for foo in Foo:
   foo.catch_phrase()
南城追梦 2025-02-01 19:44:56

为什么enummeta__准备__之外的现有成员有支票

好问题。它不再(从3.11起)。

此外,现有成员的检查之一埋在_enumdict class

no中,_enumdict in Check in 中已定义的成员

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

我喜欢您的选项4 - 但是,当然,它应该是装饰

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'

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

Good question. It no longer does (as of 3.11).

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

No, the check in _EnumDict is for members already defined in that enum:

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

I like your Option 4 -- but of course, it should be a decorator:

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

and in action:

@add_methods_to(Foo)
class MixinFoo:             # mix-in class does not need to be an enum
    def catch_phrase(self):
        return 'blah blah'
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文