Reputation: 2888
I am trying to make some constants/enum-like objects/classes with attributes in Python. Something like this.
from abc import ABC
class Entity(ABC):
# allow *labels as attributes
class Label(ABC):
@property
def spellings(self):
raise NotImplementedError
class PeriodLabel(Label):
@property
def months(self):
raise NotImplementedError
class MONTH(Entity):
class JANUARY(Label):
spellings = ['jan.', 'january']
class FEBRUARY(Label):
spellings = ['febr.', 'february']
.
.
.
class PERIOD(Entity):
class QUARTER(PeriodLabel):
spellings = ['q1', 'q2', 'q3', 'q4']
months = 3
class HALFYEAR(PeriodLabel):
spellings = ['6m']
months = 6
.
.
.
The goal is to go from MONTH
object to "MONTH"
as a str
. That part is easy since I can just use MONTH.__name__
. But I'd also like to go the opposite way from "MONTH"
to MONTH
:
assert Entity("MONTH") == MONTH
I could achieve that by doing the following but it seems to hacky I need the comparisons to be lightning fast so I think there are better ways.
class Entity(ABC):
def __new__(cls, arg):
try:
print(cls.__name__)
candidate = eval(arg)
if issubclass(candidate, cls):
return candidate
except:
pass
I would even accept assert "MONTH" == MONTH
but I need to get the class from a string. I also need to go from "MONTH.JANUARY"
to MONTH.JANUARY
. Now I have tried a bunch of different approaches but this thread is already getting out of hand.
A lot simpler approach could be
from typing import List, Optional
class Label:
def __init__(self, spellings: List[str]):
self.spellings = spellings
class Entity:
def __init__(self, **labels: Label):
for k, v in labels.items():
self.__setattr__(k, v)
def get(self, entity: str, label: Optional[str] = None):
raise NotImplementedError # todo: how?
PERIOD = Entity(Q1=Label(['q1', 'q2', 'q3', 'q4']))
assert Entity.get('PERIOD') == PERIOD
assert Entity.get('PERIOD', 'Q1') == PERIOD.Q1
Downside is it is not fullfilling as it is AND code completion wont work to reference PERIOD.Q1
since Q1
attribute is indirectly created through __setattr__
Here are a couple of examples of how it would be used. Performance is important. It is really hard to precisely explain what I want though. I hope it makes somewhat sense.
def some_function(entity_name, label_name, spellings)
print(f"{entity_name}-{label_name}:{spellings}"
# example 1
for entity in [MONTH, PERIOD, ...]:
entity_name = entity.__name__
for label in entity:
label_name = entity.__name__
some_function(entity_name, label_name, label.spellings)
# example 2 (given entity_name, label_name as strings)
entity = Entity.get(entity_name)
label = entity.get(label_name)
if entity == PERIODS:
if label.months == 3:
# do something
# example 3 (alternative to example 1)
for label in LABELS: # ALL_LABELS is auto collecting all labels
some_function(label.entity.__name__, label.__name__, label.spellings)
# example 4 (alternative to example 2)
label = LABELS.get(entity_name, label_name)
if label.entity == PERIODS:
if label.months == 3:
# do something
Upvotes: 2
Views: 2328
Reputation: 34016
One can define enums with attributes as shown for instance here. I would avoid defining my own metaclasses if the built in enum could cover my needs - here is a very rough Poc:
"""Enums Poc"""
import enum
_ALL_LABELS = set() # TODO find a way to encapsulate into Label
class Label(enum.Enum):
def __new__(cls, *args, **kwds):
value = len(cls.__members__) + 1
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, *spellings):
_ALL_LABELS.add(self)
self.spellings = spellings
class PeriodLabel(Label):
def __init__(self, months, *spellings):
super().__init__(*spellings)
self.months = months
class Entity(enum.Enum):
class MONTH(Label): # better use 'Month' here
JANUARY = ['jan.', 'january']
FEBRUARY = ['febr.', 'february']
...
class PERIOD(PeriodLabel):
QUARTER = 3, ['q1', 'q2', 'q3', 'a4']
HALFYEAR = 6, ['q1', 'q2', 'q3', 'a4']
assert Entity.PERIOD == Entity['PERIOD']
assert Entity.MONTH == Entity['MONTH']
def some_function(entity_name, label_name, spellings):
print(f"{entity_name}-{label_name}:{spellings}")
# example 1
for entity in Entity:
entity_name = entity.name
for label in entity.value: # TODO: directly iterate (not in .value)
label_name = label.name
some_function(entity_name, label_name, label.spellings)
# example 2 (given entity_name, label_name as strings)
entity_name = 'PERIOD'
entity = Entity[entity_name]
label = entity.value['QUARTER']
if entity is Entity.PERIOD:
if label.months == 3:
print('True')
Upvotes: 1
Reputation: 2888
metaclasses
to the rescue!
from __future__ import annotations
from typing import List, Dict, Tuple, Optional
class EntMeta(type):
_instances = {}
def __new__(mcs, classname: str, base_classes: Tuple[type], attrs: Dict) -> EntMeta:
qualname = attrs.get('__qualname__')
if qualname not in EntMeta._instances:
EntMeta._instances[qualname] = super().__new__(mcs, classname, base_classes, attrs)
return EntMeta._instances[qualname]
def __call__(cls, entity: str, label: Optional[str] = None) -> EntMeta:
if label is None:
qualname = entity
else:
qualname = '.'.join([entity, label])
try:
return cls._instances[qualname]
except KeyError:
raise ValueError(f"{qualname} is not a recognized entity")
class Entity(metaclass=EntMeta):
pass
class Label(metaclass=EntMeta):
@property
def spellings(self) -> List[str]:
raise NotImplementedError
class PeriodLabel(Label):
@property
def months(self) -> int:
raise NotImplementedError
class PERIOD(Entity):
class QUARTER(PeriodLabel):
spellings = ['q1', 'q2', 'q3', 'a4']
months = 3
class HALFYEAR(PeriodLabel):
spellings = ['q1', 'q2', 'q3', 'a4']
months = 6
class MONTH(Entity):
class JANUARY(Label):
spellings = ['jan.', 'january']
assert PERIOD == Entity('PERIOD')
assert MONTH == Entity('MONTH')
assert PERIOD.QUARTER == Entity('PERIOD', 'QUARTER')
assert PERIOD.HALFYEAR == Entity('PERIOD', 'HALFYEAR')
assert MONTH.JANUARY == Entity('MONTH', 'JANUARY')
Upvotes: 1