在创建后向 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 以添加成员),但在我的特定用例中,定义 MixinFoo
在 Foo
之前用作基础 class 不是一个选项(例如:Foo
是在另一个模块中定义的,我不想重新创建它或修改该模块的代码)。
到目前为止我考虑的内容:
调整 EnumMeta
。 EnumMeta
在 __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
的不同应用程序会有不同的问候语,并且可能要添加的其他功能,但没有应用程序需要添加其他 Foo
s).
选项 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 Foo
和 MixinFoo.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.member1
和 issubclass(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.member1
、FooMixin is not Foo
、issubclass(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'
问题
假设我定义了一个枚举:
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 以添加成员),但在我的特定用例中,定义 MixinFoo
在 Foo
之前用作基础 class 不是一个选项(例如:Foo
是在另一个模块中定义的,我不想重新创建它或修改该模块的代码)。
到目前为止我考虑的内容:
调整 EnumMeta
。 EnumMeta
在 __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
的不同应用程序会有不同的问候语,并且可能要添加的其他功能,但没有应用程序需要添加其他 Foo
s).
选项 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 Foo
和 MixinFoo.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.member1
和 issubclass(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.member1
、FooMixin is not Foo
、issubclass(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'