【问题标题】:Python enum prevent invalid attribute assignmentPython枚举防止无效的属性分配
【发布时间】:2019-01-20 05:59:33
【问题描述】:

当我使用函数式 API 创建枚举时,我会返回一个允许任意赋值的枚举对象(即它有一个 __dict__):

e = enum.Enum('Things',[('foo',1),('bar',2)])
e.baz = 3

该项目未出现在列表中:

list(e)
[<foo.foo: 1>, <foo.bar: 2>]

但是还是可以引用的:

if thing == e.baz: ...

现在虽然它似乎不太可能发生,但我想使用枚举的原因之一是防止拼写错误和字符串文字,并在导入模块时或尽早捕获这些东西。

有没有办法动态构建一个枚举,其行为更像是一个不允许分配任意属性的 __slots__ 对象?

【问题讨论】:

    标签: python python-3.x enums


    【解决方案1】:

    要使枚举类完全“只读”,只需要一个使用__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.
    

    【讨论】:

    • @Martijn Pieters♦ “它还为枚举提供元类和 FrozenEnum 基类;用它代替 Enum”? enum34 提供的 Enum 可以保证唯一的、恒定的值,并且不允许重新分配。我不明白为什么要使用FrozenEnum 而不是Enum?能否请您详细解释一下?
    • @Martijn Pieters♦ 为什么在给定class Color(FrozenEnum): ... red = 1 ... green = 2 ... blue = 3 ... 时允许Color.black=1;。我认为将Color 定义为Enum 类型没有区别。
    【解决方案2】:

    不一定容易,但可能。我们需要新建一个EnumMeta类型1,正常创建Enum,然后在Enum创建后重新赋值:

    from enum import Enum, EnumMeta
    
    class FrozenEnum(EnumMeta):
        "prevent creation of new attributes"
        def __getattr__(self, name):
            if name not in self._member_map_:
                raise AttributeError('%s %r has no attribute %r'
                    % (self.__class__.__name__, self.__name__, name))
            return super().__getattr__(name)
    
        def __setattr__(self, name, value):
            if name in self.__dict__ or name in self._member_map_:
                return super().__setattr__(name, value)
            raise AttributeError('%s %r has no attribute %r'
                    % (self.__class__.__name__, self.__name__, name))
    
    class Color(Enum):
        red = 1
        green = 2
        blue = 3
    
    Color.__class__ = FrozenEnum
    

    并在使用中:

    >>> type(Color)
    <class 'FrozenEnum'>
    
    >>> list(Color)
    [<Color.red: 1>, <Color.green: 2>, <Color.blue: 3>]
    
    >>> Color.blue
    <Color.blue: 3>
    
    >>> Color.baz = 3
    Traceback (most recent call last):
      ...
    AttributeError: FrozenEnum 'Color' has no attribute 'baz'
    
    >>> Color.baz
    Traceback (most recent call last):
      ...
    AttributeError: 'FrozenEnum' object has no attribute 'baz'
    

    尝试重新分配成员仍然会出现更友好的错误:

    >>> Color.blue = 9
    Traceback (most recent call last):
      ...
    AttributeError: Cannot reassign members.
    

    为了让类重赋值更容易一点,我们可以写一个装饰器来封装这个过程:

    def freeze(enum_class):
        enum_class.__class__ = FrozenEnum
        return enum_class
    

    并在使用中:

    @freeze
    class Color(Enum):
        red = 1
        green = 2
        blue = 3
    

    注意还是可以覆盖普通属性,比如函数:

    @freeze
    class Color(Enum):
        red = 1
        green = 2
        blue = 3
        def huh(self):
            print("Huh, I am %s!" % self.name)
    

    并在使用中:

    >>> Color.huh
    <function Color.huh at 0x7f7d54ae96a8>
    
    >>> Color.blue.huh()
    Huh, I am blue!
    
    >>> Color.huh = 3
    >>> Color.huh
    3
    >>> Color.blue.huh()
    Traceback (most recent call last):
      ...
    TypeError: 'int' object is not callable
    

    即使它可以被阻止,但我会(暂时)将它留给其他人作为练习。


    1 这是我见过的第二个需要子类化EnumMeta 的情况。其他见this question

    披露:我是 Python stdlib Enumenum34 backportAdvanced Enumeration (aenum) 库的作者。

    【讨论】:

    • 所有你需要让你的版本正确冻结是你的__setattr__钩子总是提出AttributeError,而不是允许设置特定的属性。这很好,因为您在创建类之后分配元类。
    • @MartijnPieters:是的,我知道。 :) 我希望新用户能看到这一点并从赏金中获得代表提升。
    • 对不起,如果我介入了你希望其他人会的地方:-) 我添加了一个完整的元类/类组合并阻止了删除,这会减轻一些失望吗? :-)
    • @MartijnPieters:嗯....如果你清理回溯(类似于我的),并修改__delattr____setattr__以通过尝试将成员值删除/更改为@987654344 @ (所以用户会得到与成员相关的错误,而不是一般的只读错误),那么我会很高兴。 :) 哦,感谢您了解__delattr__ 案例。
    猜你喜欢
    • 2019-02-01
    • 2022-01-09
    • 1970-01-01
    • 2012-09-22
    • 1970-01-01
    • 2022-06-27
    • 2014-08-30
    • 2017-12-09
    • 1970-01-01
    相关资源
    最近更新 更多