How to extend Python Enum?

后端 未结 7 1032
广开言路
广开言路 2020-12-08 18:26

What is best practice for extending Enum type in Python 3.4 and is there even a possibility for do this?

For example:

from enum import E         


        
相关标签:
7条回答
  • 2020-12-08 18:45

    I tested that way on 3.8. We may inherit existing enum but we need to do it also from base class (at last position).

    Docs:

    A new Enum class must have one base Enum class, up to one concrete data type, and as many object-based mixin classes as needed. The order of these base classes is:

    class EnumName([mix-in, ...,] [data-type,] base-enum):
        pass
    

    Example:

    class Cats(Enum):
        SIBERIAN = "siberian"
        SPHINX = "sphinx"
    
    
    class Animals(Cats, Enum):
        LABRADOR = "labrador"
        CORGI = "corgi"
    

    After that you may access Cats from Animals:

    >>> Animals.SIBERIAN
    <Cats.SIBERIAN: 'siberian'>
    

    But if you want to iterate over this enum, only new members were accessible:

    >>> list(Animals)
    [<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
    

    Actually this way is for inheriting methods from base class, but you may use it for members with these restrictions.

    Another way (a bit hacky)

    As described above, to write some function to join two enums in one. I've wrote that example:

    def extend_enum(inherited_enum):
        def wrapper(added_enum):
            joined = {}
            for item in inherited_enum:
                joined[item.name] = item.value
            for item in added_enum:
                joined[item.name] = item.value
            return Enum(added_enum.__name__, joined)
        return wrapper
    
    
    class Cats(Enum):
        SIBERIAN = "siberian"
        SPHINX = "sphinx"
    
    
    @extend_enum(Cats)
    class Animals(Enum):
        LABRADOR = "labrador"
        CORGI = "corgi"
    

    But here we meet another problems. If we want to compare members it fails:

    >>> Animals.SIBERIAN == Cats.SIBERIAN
    False
    

    Here we may compare only names and values of newly created members:

    >>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
    True
    

    But if we need iteration over new Enum, it works ok:

    >>> list(Animals)
    [<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
    

    So choose your way: simple inheritance, inheritance emulation with decorator (recreation in fact), or adding a new dependency like aenum (I haven't tested it, but I expect it support all features I described).

    0 讨论(0)
  • 2020-12-08 18:53

    I think you could do it in this way:

    import enum
    from typing import List
    from enum import Enum
    
    def extend_enum(current_enum, names: List[str], values: List = None):
        if not values:
            values = names
    
        for item in current_enum:
            names.append(item.name)
            values.append(item.value)
    
        return enum.Enum(current_enum.__name__, dict(zip(names, values)))
    
    class EventStatus(Enum):
       success = 0
       failure = 1
    
    class BookingStatus(object):
       duplicate = 2
       unknown = 3
    
    BookingStatus = extend_enum(EventStatus, ['duplicate','unknown'],[2,3])
    

    the key points is:

    • python could change anything at runtime
    • class is object too
    0 讨论(0)
  • 2020-12-08 18:57

    I've opted to use a metaclass approach to this problem.

    from enum import EnumMeta
    
    class MetaClsEnumJoin(EnumMeta):
        """
        Metaclass that creates a new `enum.Enum` from multiple existing Enums.
    
        @code
            from enum import Enum
    
            ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
            ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
            class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
                pass
    
            print(ENUMJOINED.a)
            print(ENUMJOINED.b)
            print(ENUMJOINED.c)
            print(ENUMJOINED.d)
        @endcode
        """
    
        @classmethod
        def __prepare__(metacls, name, bases, enums=None, **kargs):
            """
            Generates the class's namespace.
            @param enums Iterable of `enum.Enum` classes to include in the new class.  Conflicts will
                be resolved by overriding existing values defined by Enums earlier in the iterable with
                values defined by Enums later in the iterable.
            """
            #kargs = {"myArg1": 1, "myArg2": 2}
            if enums is None:
                raise ValueError('Class keyword argument `enums` must be defined to use this metaclass.')
            ret = super().__prepare__(name, bases, **kargs)
            for enm in enums:
                for item in enm:
                    ret[item.name] = item.value  #Throws `TypeError` if conflict.
            return ret
    
        def __new__(metacls, name, bases, namespace, **kargs):
            return super().__new__(metacls, name, bases, namespace)
            #DO NOT send "**kargs" to "type.__new__".  It won't catch them and
            #you'll get a "TypeError: type() takes 1 or 3 arguments" exception.
    
        def __init__(cls, name, bases, namespace, **kargs):
            super().__init__(name, bases, namespace)
            #DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older.  You'll get a
            #"TypeError: type.__init__() takes no keyword arguments" exception.
    

    This metaclass can be used like so:

    >>> from enum import Enum
    >>>
    >>> ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
    >>> ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
    >>> class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
    ...     e = 5
    ...     f = 6
    ...
    >>> print(repr(ENUMJOINED.a))
    <ENUMJOINED.a: 1>
    >>> print(repr(ENUMJOINED.b))
    <ENUMJOINED.b: 2>
    >>> print(repr(ENUMJOINED.c))
    <ENUMJOINED.c: 3>
    >>> print(repr(ENUMJOINED.d))
    <ENUMJOINED.d: 4>
    >>> print(repr(ENUMJOINED.e))
    <ENUMJOINED.e: 5>
    >>> print(repr(ENUMJOINED.f))
    <ENUMJOINED.f: 6>
    

    This approach creates a new Enum using the same name-value pairs as the source Enums, but the resulting Enum members are still unique. The names and values will be the same, but they will fail direct comparisons to their origins following the spirit of Python's Enum class design:

    >>> ENUMA.b.name == ENUMJOINED.b.name
    True
    >>> ENUMA.b.value == ENUMJOINED.b.value
    True
    >>> ENUMA.b == ENUMJOINED.b
    False
    >>> ENUMA.b is ENUMJOINED.b
    False
    >>>
    

    Note what happens in the event of a namespace conflict:

    >>> ENUMC = Enum('ENUMA', {'a': 1, 'b': 2})
    >>> ENUMD = Enum('ENUMB', {'a': 3})
    >>> class ENUMJOINEDCONFLICT(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMC, ENUMD)):
    ...     pass
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 19, in __prepare__
      File "C:\Users\jcrwfrd\AppData\Local\Programs\Python\Python37\lib\enum.py", line 100, in __setitem__
        raise TypeError('Attempted to reuse key: %r' % key)
    TypeError: Attempted to reuse key: 'a'
    >>>
    

    This is due to the base enum.EnumMeta.__prepare__ returning a special enum._EnumDict instead of the typical dict object that behaves different upon key assignment. You may wish to suppress this error message by surrounding it with a try-except TypeError, or there may be a way to modify the namespace before calling super().__prepare__(...).

    0 讨论(0)
  • 2020-12-08 19:02

    Calling the Enum class directly and making use of chain allows the extension (joining) of an existing enum.

    I came upon the problem of extending enums while working on a CANopen implementation. Parameter indices in the range from 0x1000 to 0x2000 are generic to all CANopen nodes while e.g. the range from 0x6000 onwards depends open whether the node is a drive, io-module, etc.

    nodes.py:

    from enum import IntEnum
    
    class IndexGeneric(IntEnum):
        """ This enum holds the index value of genric object entrys
        """
        DeviceType    = 0x1000
        ErrorRegister = 0x1001
    
    Idx = IndexGeneric
    

    drives.py:

    from itertools import chain
    from enum import IntEnum
    from nodes import IndexGeneric
    
    class IndexDrives(IntEnum):
        """ This enum holds the index value of drive object entrys
        """
        ControlWord   = 0x6040
        StatusWord    = 0x6041
        OperationMode = 0x6060
    
    Idx= IntEnum('Idx', [(i.name, i.value) for i in chain(IndexGeneric,IndexDrives)])
    
    0 讨论(0)
  • 2020-12-08 19:02

    Another way :

    Letter = Enum(value="Letter", names={"A": 0, "B": 1})
    LetterExtended = Enum(value="Letter", names=dict({"C": 2, "D": 3}, **{i.name: i.value for i in Letter}))
    

    Or :

    LetterDict = {"A": 0, "B": 1}
    Letter = Enum(value="Letter", names=LetterDict)
    
    LetterExtendedDict = dict({"C": 2, "D": 3}, **LetterDict)
    LetterExtended = Enum(value="Letter", names=LetterExtendedDict)
    

    Output :

    >>> Letter.A
    <Letter.A: 0>
    >>> Letter.C
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
      File "D:\jhpx\AppData\Local\Programs\Python\Python36\lib\enum.py", line 324, in __getattr__
        raise AttributeError(name) from None
    AttributeError: C
    >>> LetterExtended.A
    <Letter.A: 0>
    >>> LetterExtended.C
    <Letter.C: 2>
    
    0 讨论(0)
  • 2020-12-08 19:06

    While uncommon, it is sometimes useful to create an enum from many modules. The aenum1 library supports this with an extend_enum function:

    from aenum import Enum, extend_enum
    
    class Index(Enum):
        DeviceType    = 0x1000
        ErrorRegister = 0x1001
    
    for name, value in (
            ('ControlWord', 0x6040),
            ('StatusWord', 0x6041),
            ('OperationMode', 0x6060),
            ):
        extend_enum(Index, name, value)
    
    assert len(Index) == 5
    assert list(Index) == [Index.DeviceType, Index.ErrorRegister, Index.ControlWord, Index.StatusWord, Index.OperationMode]
    assert Index.DeviceType.value == 0x1000
    assert Index.StatusWord.value == 0x6041
    

    1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

    0 讨论(0)
提交回复
热议问题