1

I know, I know, there are already similar questions in here and there. But their questions and answers are not exactly what I am looking for. Besides, they are locked question so I can not add a new answer to them. SMH.

Firstly, let's clarify the question to understand its scope. When using enum in other static languages like this:

public enum Size
{
    SMALL=0,
    MIDIUM=1,
    LARGE=2,
    BIG=2  // There can possibly be an alias
}

we want it to help us in:

  1. Guard against typo when referencing a value. For example, var foo = Size.SMALL is valid, var bar = Size.SMAL should generate a lousy error.
  2. Enum values can support strings, Such as HTTP404 = "Not Found", HTTP200 = "OK", .... (Therefore those implementations based on range(N) is unacceptable.)
  3. When defining a parameter as a specific Enum type, it serves as a regulation to accept only that kind of values. For example, public void Foo(Size size) {...}
  4. I also want the values to be first-class citizen in my Enum solution. Meaning, my functions def parser(value_from_the_wire): ... would like to consume some native values (such as an integer or a string etc.), rather than to consume an Enum member. This is the tricky part in standard Enum in Python 3:

    • assert 2 == MY_ENUM.MY_VALUE would only work when MY_ENUM was derived from IntEnum (and there is no default StrEnum although it is not difficult to subclass one by yourself)
    • assert 2 in MY_ENUM wouldn't work, even if MY_ENUM was derived from IntEnum.
RayLuo
  • 17,257
  • 6
  • 88
  • 73
  • 1
    I don't understand how your question is different from the ones you linked. Why don't the answers there answer your question? – Aran-Fey Sep 25 '18 at 08:58
  • @Aran-Fey None of those questions clarified the requirement, therefore, their answers tended to based on different assumption in the different answerer's mind. – RayLuo Sep 25 '18 at 09:04
  • Huh? What's there to clarify? The question "How do I represent an Enum" is pretty clear, I think. What can your enum do that the other enums in the existing answers can't? – Aran-Fey Sep 25 '18 at 09:06
  • Well, when I post this Q&A, at least I did my homework. Did you do yours before posting your comments? Most of the naive solution `class MY_ENUM: NAME1 = "value1"` does not satisfy my requirement #3, i.e. to allow an `if input_value in MY_ENUM: ...` check. Some others has an assumption "If you need the numeric values, here's the quickest way: `dog, cat, rabbit = range(3)`", which is not what I want. etc. etc. – RayLuo Sep 25 '18 at 09:13
  • 1
    Are you serious? All the problems you've listed have nothing to do with the question. Those problems only exist if you choose a bad way to represent your enum. All of those are problems with the *answers*, not the *question*. If you see a bad answer, downvote it. Your question is not any different from the other question. – Aran-Fey Sep 25 '18 at 09:20
  • 1. So when you wrote your first comment here, you were asking "why don't the answers there answer my question". Now you seem to know why, and then your suggestion is asking me to downvote all those answers because they happen to not meet my need? 2. I also explained that I would otherwise love to add my answer to those questions, but they are locked. 3. If my question clarifies the potential requirements clearly, wouldn't it provide extra value to others? Why do you have a problem with that? Well, actually, I don't really need to know your thoughts on them. Take care. – RayLuo Sep 25 '18 at 09:30
  • 1. Some of the answers there answer your question perfectly. Only the bad ones don't. 2) Ok. I don't think it's appropriate to create a duplicate question just because you want to add an answer to a locked question, though. 3) Why couldn't you just edit the existing question if you felt a need to clarify the requirements? – Aran-Fey Sep 25 '18 at 09:35
  • I would disagree. Those existing questions were already asked in a very broad way, and then accumulated lots of educational answers since then. It is just that they do not fit the specific case I describe here. Also, at this point, it is not appropriate either to alter their question by narrowing its scope, and then implicitly make many of the existing answers become irrelevant. Last but not the least, a specific question adds value by NOT being a [too broad question](https://meta.stackoverflow.com/questions/258589/breaking-down-too-broad-and-trying-to-understand-it). So it is not a duplicate. – RayLuo Sep 25 '18 at 10:05
  • "I also want the values to be first-class citizen in my Enum solution" - but `enum.Enum` does that. `enum.Enum` values are first-class, fully-featured objects. You can define methods on them, you can distinguish them from ordinary ints, etc. Your elaboration and your self-answer suggest that you want your enum to be a mere collection of aliases for ordinary ints and strings. – user2357112 Oct 01 '18 at 00:57
  • If you want to go from `Size.SMALL` to `0` and vice versa, that's already possible with `enum.Enum` using `Size.SMALL.value` and `Size(0)`. – user2357112 Oct 01 '18 at 01:02
  • @RayLuo: Your 4a is wrong on two counts: 1) the attribute is `.value`, not `.MY_VALUE`; and 2) `assert 2 == MY_ENUM.value` works as long as the value is '2' (not `3`, not 'red', etc.) - whether or not `MY_ENUM` is an `Enum` or an `IntEnum`. – Ethan Furman Oct 02 '18 at 03:34

2 Answers2

1

TL;DR: Use venum

So my Python solution to satisfy the 3 criterias in the question, is based on namedtuple and the implementation seems more straightforward than the new built-in Enum in Python 3.

from collections import namedtuple

def enum(name=None, **kwargs):
    """
    :param name: An optional type name, which only shows up when debugging by print(...)
    """
    # This actual implementation below is just a one-liner, even within 80-char
    return namedtuple(name or "Const_%d" % id(kwargs), kwargs.keys())(**kwargs)

Usage is now simple.

# definition syntax
SIZE = enum("Size", SMALL=0, MEDIUM=1, LARGE=2, BIG=2)

# usage on referencing
print(SIZE.SMALL)   # got 0, instead of <SIZE.SMALL: 0>
try:
    print(SIZE.SMAL)    # got AttributeError
    assert False, "should not arrive this line"
except AttributeError:
    pass

# usage on comparison and contains-check
assert SIZE.MEDIUM == 1  # works. It won't work when using standard Enum (unless using IntEnum)
assert 1 in SIZE  # works. It won't work when using standard Enum (unless you wrote it as SIZE(1)).

# usage on regulating input value
def t_shirt_size(size):

    if size not in SIZE:
        raise ValueError("Invalid input value")

    print("Placing order with size: %s" % size)

t_shirt_size(SIZE.MEDIUM)   # works
t_shirt_size(2)             # also want this to work
try:
    t_shirt_size(7)             # got AssertionError
    assert False, "This line should not be reached!"
except ValueError:
    pass

EDIT 1: I was actually aware that there is a standard Enum module in Python 3 which is, feature-wise speaking, largely a superset to my one-liner implementation below. However there is one scenario that the standard Enum won't suit my need. I want the values to be a first-class citizen in my enum; I want my t_shirt_size(...) function to accept a real value, not just an enum member. The standard enum approach would NOT allow these 2 usages: assert SIZE.MEDIUM == 1 nor assert 1 in SIZE.

EDIT 2: Given that people tend to stereotype this topic as a duplicate, I planned to actually implement my approach as a standalone module with plenty of documentation. I even came up a cool name for it, venum, V stands for Value. It was at that time that I checked the name in pypi and found out there is already a package with that same name, using the same approach as mine, and documented well. So that settles it. I'll simply pip install venum instead. :-)

RayLuo
  • 17,257
  • 6
  • 88
  • 73
  • `enum.Enum` doesn't force you to have your function accept enum members. With `Size` as an `enum.Enum` enum, you could accept `size` as an int and call `Size(size)` to get an enum member. – user2357112 Oct 01 '18 at 01:03
  • @user2357112, thanks, good to know that there is such a syntactic sugar from standard Enum: `SIZE(value)` as equivalent as `if value not in SIZE: raise ValueError("...")`. Personally I think the latter is more pythonic though, because ["Explicit is better than implicit"](https://www.python.org/dev/peps/pep-0020/). – RayLuo Oct 01 '18 at 01:11
  • @RayLuo: Everything has its place -- one does not, for example, say `if my_key not in some_dict: raise KeyError()` even though it is more explicit than `some_dict[my_key]`. – Ethan Furman Oct 02 '18 at 03:51
  • @EthanFurman: Well, if "everything has its place", why did you not allow a different style answer to have its place? – RayLuo Oct 06 '18 at 19:35
0

Implementing with Python 3's Enum:

from enum import IntEnum


class SIZE(Enum):

    SMALL = 0
    MEDIUM = 1
    LARGE = 2
    BIG = 2

    @classmethod
    def contains(cls, value):
        return any([e.value == value for e in cls])

and using:

print(SIZE.SMALL)   # got <SIZE.SMALL: 0>
print(SIZE.SMAL)    # got AttributeError

def t_shirt_size(size):
    assert size in SIZE, "Invalid input value"
    place_order_with_size(size)

t_shirt_size(SIZE.MEDIUM)   # works
t_shirt_size(7)             # got AssertionError
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Thanks for the down vote. I was actually aware that there is a standard `Enum` module in Python 3 that is, feature-wise speaking, largely a superset to my one-liner implementation. However there is one scenario that it won't suit my need. I want the values to be a first-class citizen in my enum; I want my `t_shirt_size(...)` function to accept a real value, not just an enum member. The standard enum approach would NOT allow these 2 usages: `assert SIZE.MEDIUM == 1` nor `assert 1 in SIZE`. Now, your particular answer does not suit my need. Yet I ain't gonna downvote it. Would you be fair too? – RayLuo Sep 30 '18 at 23:59
  • @RayLuo: `assert SIZE.MEDIUM == 1` does work with an `IntEnum`. The second one does not work as-is, but you can add your own method to do the heavy-lifting. – Ethan Furman Oct 02 '18 at 03:40
  • @RayLuo: Portions of your answer are wrong -- that is why I down-voted it. Nothing in my answer is wrong, and it now meets your needs of direct value comparison and containment checks. – Ethan Furman Oct 02 '18 at 03:48
  • even though `assert SIZE.MEDIUM == 1` does work with `IntEnum`, which I already knew and pointed out in my question; one of my use case is to also support string and there is no default `StrEnum`, which again I already pointed out. I was just seeking a way to simply treat values as first class citizen and I found a solution I need, so by definition my answer was correct, I was just humble to not (yet?) accept it, to encourage different voices. Now, how confident are you to say my answer was wrong (to me) and yours are correct (to me)? Sorry I don't think so. – RayLuo Oct 06 '18 at 19:44
  • @RayLuo: As I said in my comment, *portions* of your answer are incorrect -- as in technically incorrect. Fix those and I'll remove my downvote. – Ethan Furman Oct 07 '18 at 17:16