要使枚举类完全“只读”,只需要一个使用__setattr__ hook 的元类即可防止所有 属性分配。因为元类是在创建之后附加到类,所以分配正确的枚举值没有问题。
就像 Ethan 的回答一样,我使用 EnumMeta 类作为自定义元类的基础:
from enum import EnumMeta, Enum
class FrozenEnumMeta(EnumMeta):
"Enum metaclass that freezes an enum entirely"
def __new__(mcls, name, bases, classdict):
classdict['__frozenenummeta_creating_class__'] = True
enum = super().__new__(mcls, name, bases, classdict)
del enum.__frozenenummeta_creating_class__
return enum
def __call__(cls, value, names=None, *, module=None, **kwargs):
if names is None: # simple value lookup
return cls.__new__(cls, value)
enum = Enum._create_(value, names, module=module, **kwargs)
enum.__class__ = type(cls)
return enum
def __setattr__(cls, name, value):
members = cls.__dict__.get('_member_map_', {})
if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
return super().__setattr__(name, value)
if hasattr(cls, name):
msg = "{!r} object attribute {!r} is read-only"
else:
msg = "{!r} object has no attribute {!r}"
raise AttributeError(msg.format(cls.__name__, name))
def __delattr__(cls, name):
members = cls.__dict__.get('_member_map_', {})
if hasattr(cls, '__frozenenummeta_creating_class__') or name in members:
return super().__delattr__(name)
if hasattr(cls, name):
msg = "{!r} object attribute {!r} is read-only"
else:
msg = "{!r} object has no attribute {!r}"
raise AttributeError(msg.format(cls.__name__, name))
class FrozenEnum(Enum, metaclass=FrozenEnumMeta):
pass
上面区分了已经可用的属性和新的属性,以便于诊断。它还阻止属性删除,这可能同样重要!
它还为枚举提供了元类和FrozenEnum基类;用这个代替Enum。
冻结样本Color枚举:
>>> class Color(FrozenEnum):
... red = 1
... green = 2
... blue = 3
...
>>> list(Color)
[<Color.red: 1>, <Color.green: 2>, <Color.blue: 3>]
>>> Color.foo = 'bar'
Traceback (most recent call last):
# ...
AttributeError: 'Color' object has no attribute 'foo'
>>> Color.red = 42
Traceback (most recent call last):
# ...
Cannot reassign members.
>>> del Color.red
Traceback (most recent call last):
# ...
AttributeError: Color: cannot delete Enum member.
请注意,所有属性更改都是不允许的,不允许新属性,删除也被阻止。当名称是枚举成员时,我们将委托给原始的 EnumMeta 处理以保持错误消息的稳定。
如果你的枚举使用了改变枚举类属性的属性,你要么必须将它们列入白名单,要么允许设置以单个下划线开头的名称;在__setattr__ 中确定允许设置哪些名称并将super().__setattr__(name, value) 用于这些异常,就像现在代码通过使用标志属性区分类构造和以后的更改一样。
上面的类可以像Enum()一样用来以编程方式创建一个枚举:
e = FrozenEnum('Things', [('foo',1), ('bar',2)]))
演示:
>>> e = FrozenEnum('Things', [('foo',1), ('bar',2)])
>>> e
<enum 'Things'>
>>> e.foo = 'bar'
Traceback (most recent call last):
# ...
AttributeError: Cannot reassign members.