Can I have one alias for two distinct members of my enum

Viewed 123

I sincerely hope, that I followed all the rules and guidelines and that my question/problem is comprehensive enough to find an adequate answer. First, a small intro:
For a current project of mine I want to designate specific types to tree node objects. To keep matters simple here, I will use colours as an analogy.

What I have

Let's suppose I have the following enum, derived from the enum standard library:

from enum import Enum, auto

class Colour(Enum):
  CRIMSON = auto()
  SCARLET = auto()
  RED = auto()
  BLUE = auto()

So each member has a different value.

What I want

So far, so good. But what I would like to have is this:

class Colour(Enum):
  CRIMSON = auto()
  SCARLET = auto()
  # RED = CRIMSON or SCARLET
  BLUE = auto()

So if I have an object with an attribute like this:

>>> r = Colour.SCARLET

I want the following statements to return True:

>>> type(r) is Colour
True
>>> r is Colour.SCARLET
True
>>> r is Colour.RED
True

and of course this should be False:

>>> r is Colour.CRIMSON
False
>>> r is Colour.BLUE # obviously False...
False

Note that it is not neccessary in my case to be able to assign the type RED, only assigning CRIMSON or SCARLET would be sufficient.

My current (unsatisfying) solution

What I tried so far is the following:

class Colour(Enum):
# ... member definitions ...
  def isRed(self):
    return self is Colour.SCARLET or self is Colour.CRIMSON

But I find this solution rather inelegant and quite a hassle to use. Also it does not reflect the nature of CRIMSON and SCARLET as shades of RED.

Another solution I implemented involved subcalssing or nesting Colour but in either case I was not able to meet all the criteria above, hence I refrained from posting these here.

Before I used enums, I represented the Node.colour property for each node with integers:

# trees.py
# defining global constants
SCARLET = 1
CRIMSON = 2
BLUE = -1

class Node:
  def __init__(self, parent, children, col):
    self.parent = parent
    self.children = children
    self.colour = col # either 1, 2 or -1

So I could do the following:

>>> x = Node(None, None, SCARLET)
>>> y = Node(None, None, BLUE)
>>> If x.colour is > 0:# do stuff...

but I found that way very unpythonic and rather unintutive as well, since testing for RED, SCARLET, BLUE etc. occurs rather often and this makes the code somewhat harder to read.
Also one should not use int data-type if not necessary in my opinion since these "colours" have no natural ordering or other int-like properties.

1 Answers

To make sure I understand:

Colour.SCARLET is Color.RED
Colour.CRIMSON is color.RED

but

Colour.SCARLET is not Colour.CRIMSON

?

If that is correct, then no, you cannot have that. The reason is that is is object identity, and there are no work-arounds or tricks to fool it. This is intrinsic to Python, not specific to Enums.

Can SCARLET and CRIMSON be aliases of RED, or do they have unique properties, one of which is being a red shade?

Something that might work for you (shown using the aenum library, but doable with slightly more work using the stdlib Enum):

from aenum import Enum, auto

class PrimaryColour(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()

class Shades(Enum):
    #
    _init_ = 'value primary'
    #
    CRIMSON = PrimaryColour.RED
    SCARLET = PrimaryColour.RED

and in use:

>>> list(Shades)
[<Shades.CRIMSON: 1>, <Shades.SCARLET: 2>]

>>> Shades.CRIMSON.primary is PrimaryColour.RED
True

Another possibility:

from enum import Enum, auto

class Colour(Enum):
    #
    RED = auto()
    GREEN = auto()
    BLUE = auto()
    CRIMSON = RED
    SCARLET = RED
    AQUAMARINE = GREEN, BLUE
    #
    def is_colour(self, colour):
        return colour._value_ in self.ancestors or self._value_ in colour.ancestors
    #
    def __new__(cls, *values):
        members = list(cls)
        used_values = {m.value: m for m in members}
        # previous values are now in used_values; after isolating first
        # value, check if it has already been used
        first, *others = values
        if first in used_values:
            # it's an alias -- create a new value
            others = (first, ) + tuple(others)
            first = len(members) + 1
        member = object.__new__(cls)
        member._value_ = first
        member.ancestors = (first, ) + tuple(others)  # always it's own ancestor
        return member

and in use:

>>> list(Colour)
[<Colour.RED: 1>, <Colour.GREEN: 2>, <Colour.BLUE: 3>, <Colour.CRIMSON: 4>, <Colour.SCARLET: 5>, <Colour.AQUAMARINE: 6>]

>>> Colour.RED
<Colour.RED: 1>

>>> Colour.CRIMSON
<Colour.CRIMSON: 4>

>>> Colour.AQUAMARINE
<Colour.AQUAMARINE: 6>

>>> Colour.CRIMSON.is_colour(Colour.RED)
True

>>> Colour.RED.is_colour(Colour.CRIMSON)
True

>>> Colour.CRIMSON.is_colour(Colour.SCARLET)
False
Related