When should I subclass EnumMeta instead of Enum?
The best (and only) cases I have seen so far for subclassing EnumMeta
comes from these four questions:
A more pythonic way to define an enum with dynamic members
Prevent invalid enum attribute assignment
Create an abstract Enum class
Invoke a function when an enum member is accessed
We'll examine the dynamic member case further here.
First, a look at the code needed when not subclassing EnumMeta
:
The stdlib way
from enum import Enum
import json
class BaseCountry(Enum):
def __new__(cls, record):
member = object.__new__(cls)
member.country_name = record['name']
member.code = int(record['country-code'])
member.abbr = record['alpha-2']
member._value_ = member.abbr, member.code, member.country_name
if not hasattr(cls, '_choices'):
cls._choices = {}
cls._choices[member.code] = member.country_name
cls._choices[member.abbr] = member.country_name
return member
def __str__(self):
return self.country_name
Country = BaseCountry(
'Country',
[(rec['alpha-2'], rec) for rec in json.load(open('slim-2.json'))],
)
The aenum
way 1 2
from aenum import Enum, MultiValue
import json
class Country(Enum, init='abbr code country_name', settings=MultiValue):
_ignore_ = 'country this' # do not add these names as members
# create members
this = vars()
for country in json.load(open('slim-2.json')):
this[country['alpha-2']] = (
country['alpha-2'],
int(country['country-code']),
country['name'],
)
# have str() print just the country name
def __str__(self):
return self.country_name
The above code is fine for a one-off enumeration -- but what if creating Enums from JSON files was common for you? Imagine if you could do this instead:
class Country(JSONEnum):
_init_ = 'abbr code country_name' # remove if not using aenum
_file = 'some_file.json'
_name = 'alpha-2'
_value = {
1: ('alpha-2', None),
2: ('country-code', lambda c: int(c)),
3: ('name', None),
}
As you can see:
_file
is the name of the json file to use_name
is the path to whatever should be used for the name_value
is a dictionary mapping paths to values3_init_
specifies the attribute names for the different value components (if usingaenum
)
The JSON data is taken from https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes -- here is a short excerpt:
[{"name":"Afghanistan","alpha-2":"AF","country-code":"004"},
{"name":"Åland Islands","alpha-2":"AX","country-code":"248"},
{"name":"Albania","alpha-2":"AL","country-code":"008"},
{"name":"Algeria","alpha-2":"DZ","country-code":"012"}]
Here is the JSONEnumMeta
class:
class JSONEnumMeta(EnumMeta):
@classmethod
def __prepare__(metacls, cls, bases, **kwds):
# return a standard dictionary for the initial processing
return {}
def __init__(cls, *args , **kwds):
super(JSONEnumMeta, cls).__init__(*args)
def __new__(metacls, cls, bases, clsdict, **kwds):
import json
members = []
missing = [
name
for name in ('_file', '_name', '_value')
if name not in clsdict
]
if len(missing) in (1, 2):
# all three must be present or absent
raise TypeError('missing required settings: %r' % (missing, ))
if not missing:
# process
name_spec = clsdict.pop('_name')
if not isinstance(name_spec, (tuple, list)):
name_spec = (name_spec, )
value_spec = clsdict.pop('_value')
file = clsdict.pop('_file')
with open(file) as f:
json_data = json.load(f)
for data in json_data:
values = []
name = data[name_spec[0]]
for piece in name_spec[1:]:
name = name[piece]
for order, (value_path, func) in sorted(value_spec.items()):
if not isinstance(value_path, (list, tuple)):
value_path = (value_path, )
value = data[value_path[0]]
for piece in value_path[1:]:
value = value[piece]
if func is not None:
value = func(value)
values.append(value)
values = tuple(values)
members.append(
(name, values)
)
# get the real EnumDict
enum_dict = super(JSONEnumMeta, metacls).__prepare__(cls, bases, **kwds)
# transfer the original dict content, _items first
items = list(clsdict.items())
items.sort(key=lambda p: (0 if p[0][0] == '_' else 1, p))
for name, value in items:
enum_dict[name] = value
# add the members
for name, value in members:
enum_dict[name] = value
return super(JSONEnumMeta, metacls).__new__(metacls, cls, bases, enum_dict, **kwds)
# for use with both Python 2/3
JSONEnum = JSONEnumMeta('JsonEnum', (Enum, ), {})
A few notes:
JSONEnumMeta.__prepare__
returns a normaldict
EnumMeta.__prepare__
is used to get an instance of_EnumDict
-- this is the proper way to get onekeys with a leading underscore are passed to the real
_EnumDict
first as they may be needed when processing the enum membersEnum members are in the same order as they were in the file
1 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
2 This requires aenum 2.0.5+
.
3 The keys are numeric to keep multiple values in order should your Enum
need more than one.
How to get new Enum with members as enum using EnumMeta Python 3.6
In Python 3, the dct
object is not a regular dictionary, but a subclass dedicated to helping create enums, set via the __prepare__
attribute of the metaclass. It's update()
method, however, is not altered from the base dictionary.
It normally expects members to be set via it's __setitem__
method (e.g. dct[name] = value
); do so in your __new__
method too:
class KeyMeta(EnumMeta):
def __new__(mcs, name, bases, dct):
for e in KeyCode:
dct[e.name] = e.value
return super(KeyMeta, mcs).__new__(mcs, name, bases, dct)
Now each of the names A
, B
and C
are recorded as being enum member names and the EnumMeta
class takes it from there. Without using the specialised __setitem__
implementation, the names are seen as some other type of attribute instead.
With the above change you get the expected output:
>>> class Key(Enum, metaclass=KeyMeta):
... pass
...
>>> Key.A
<Key.A: 1>
The same for e in KeyCode:
loop will continue to work in Python 2 as well.
Overload __init__() for a subclass of Enum
You have to overload the _missing_
hook. All instances of WeekDay
are created when the class is first defined; WeekDay(date(...))
is an indexing operation rather than a creation operation, and __new__
is initially looking for pre-existing values bound to the integers 0 to 6. Failing that, it calls _missing_
, in which you can convert the date
object into such an integer.
class WeekDay(Enum):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
@classmethod
def _missing_(cls, value):
if isinstance(value, date):
return cls(value.weekday())
return super()._missing_(value)
A few examples:
>>> WeekDay(date(2019,3,7))
<WeekDay.THURSDAY: 3>
>>> assert WeekDay(date(2019, 4, 1)) == WeekDay.MONDAY
>>> assert WeekDay(date(2019, 4, 3)) == WeekDay.MONDAY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
(Note: _missing_
is not available prior to Python 3.6.)
Prior to 3.6, it seems you can override EnumMeta.__call__
to make the same check, but I'm not sure if this will have unintended side effects. (Reasoning about __call__
always makes my head spin a little.)
# Silently convert an instance of datatime.date to a day-of-week
# integer for lookup.
class WeekDayMeta(EnumMeta):
def __call__(cls, value, *args, **kwargs):
if isinstance(value, date):
value = value.weekday())
return super().__call__(value, *args, **kwargs)
class WeekDay(Enum, metaclass=WeekDayMeta):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
How to extend the Python class attributes for Enum derived classes
Enum
was designed to not allow extending. Depending on your use-case, though, you have a couple options:
- build the enum dynamically from an external source (such as a json file). See When should I subclass EnumMeta instead of Enum? for the full details.
class Country(JSONEnum):
_init_ = 'abbr code country_name' # remove if not using aenum
_file = 'some_file.json'
_name = 'alpha-2'
_value = {
1: ('alpha-2', None),
2: ('country-code', lambda c: int(c)),
3: ('name', None),
}
- use the
extend_enum
function theaenum
library1:
extend_enum(ErrorCode, 'NEW_ERROR2', 'The new error 2')
1 Disclosure: I am the author of the Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) library.
Create an abstract Enum class
Here's how to adapt the accepted answer to the question Abstract Enum Class using ABCMeta and EnumMeta to create the kind of abstract Enum
class you want:
from abc import abstractmethod, ABC, ABCMeta
from enum import auto, Flag, EnumMeta
class ABCEnumMeta(ABCMeta, EnumMeta):
def __new__(mcls, *args, **kw):
abstract_enum_cls = super().__new__(mcls, *args, **kw)
# Only check abstractions if members were defined.
if abstract_enum_cls._member_map_:
try: # Handle existence of undefined abstract methods.
absmethods = list(abstract_enum_cls.__abstractmethods__)
if absmethods:
missing = ', '.join(f'{method!r}' for method in absmethods)
plural = 's' if len(absmethods) > 1 else ''
raise TypeError(
f"cannot instantiate abstract class {abstract_enum_cls.__name__!r}"
f" with abstract method{plural} {missing}")
except AttributeError:
pass
return abstract_enum_cls
class TranslateableFlag(Flag, metaclass=ABCEnumMeta):
@classmethod
@abstractmethod
def base(cls):
pass
def translate(self):
base = self.base()
if self in base:
return base[self]
else:
ret = []
for basic in base:
if basic in self:
ret.append(base[basic])
return " | ".join(ret)
class Students1(TranslateableFlag):
ALICE = auto()
BOB = auto()
CHARLIE = auto()
ALL = ALICE | BOB | CHARLIE
@classmethod
def base(cls):
return {Students1.ALICE: "Alice", Students1.BOB: "Bob",
Students1.CHARLIE: "Charlie"}
# Abstract method not defined - should raise TypeError.
class Students2(TranslateableFlag):
ALICE = auto()
BOB = auto()
CHARLIE = auto()
ALL = ALICE | BOB | CHARLIE
# @classmethod
# def base(cls):
# ...
Result:
Traceback (most recent call last):
...
TypeError: cannot instantiate abstract class 'Students2' with abstract method 'base'
Python: overloading the `__call__` dunder method of a class that inherits Enum
The typical way to look up an enum member by name is with the __getitem__
syntax:
>>> B["BOB"]
<B.BOB: 1>
If you need to be able to do value lookups using the name, you can define _missing_
:
class A(Enum):
#
@classmethod
def _missing_(cls, name):
return cls[name]
class B(A):
BOB = 1
and in use:
>>> B("BOB")
<B.BOB: 1>
How to extend Python Enum?
Subclassing an enumeration is allowed only if the enumeration does not define any members.
Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances.
https://docs.python.org/3/library/enum.html#restricted-enum-subclassing
So no, it's not directly possible.
Related Topics
How to Use Groupby to Concatenate Strings in Python Pandas
Socket.Shutdown VS Socket.Close
Unnamed Python Objects Have the Same Id
How to Qcut with Non Unique Bin Edges
Comparing Boolean and Int Using Isinstance
How to Hide the Console Window in a Pyqt App Running on Windows
How to Automatically Fix an Invalid JSON String
Getting Today's Date in Yyyy-Mm-Dd in Python
Call Int() Function on Every List Element
How to Separate the Functions of a Class into Multiple Files
Rename Specific Column(S) in Pandas
List of Tables, Db Schema, Dump etc Using the Python SQLite3 API
Speed of Calculating Powers (In Python)
Why Do "Not a Number" Values Equal True When Cast as Boolean in Python/Numpy