2

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.

EDIT1

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__

EDIT2

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
9
  • 2
    Have you checked native enum.Enum class? docs.python.org/3/library/enum.html Commented Jul 5, 2019 at 19:48
  • Yes but I can't figure out how to solve it with that either Commented Jul 5, 2019 at 19:52
  • Take a look at metaclasses. A metaclass constructs the class from attribute declarations; you can rework your attributes however you want, turn them into properties, register them for easy search by name, etc. Commented Jul 5, 2019 at 20:05
  • Could you add some examples of how these would actually be used? Commented Jul 5, 2019 at 20:30
  • @9000 seems like a good advice. I think I might be able to do it with that. Didn’t know about metaclasses before Commented Jul 5, 2019 at 21:30

2 Answers 2

1

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')
Sign up to request clarification or add additional context in comments.

Comments

1

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')

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.