classification
Title: Use enum names as values in enum.Enum() functional API
Type: enhancement Stage: resolved
Components: Versions: Python 3.4
process
Status: closed Resolution: rejected
Dependencies: 17947 Superseder:
Assigned To: ethan.furman Nosy List: barry, eli.bendersky, ethan.furman, ncoghlan, pitrou
Priority: normal Keywords:

Created on 2013-05-12 10:57 by ncoghlan, last changed 2013-06-19 14:07 by pitrou. This issue is now closed.

Messages (25)
msg189013 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-05-12 10:57
I encountered an interesting suggestion [1] regarding the enum.Enum convenience API: use the member names as their values, rather than the current integers starting from one.

Since we're now using definition order rather than value order for iteration, this suggestion makes a lot of sense to me.

(again, posting this as something to consider after the accepted PEP is incorporated)

[1] http://www.acooke.org/cute/Pythonssad0.html
msg189022 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-05-12 12:28
Specifically, my suggestion is that for auto-created enum members, the invariant "asset x.value == str(x)" should hold.

In flufl.enum, using integers made sense because it relies on sorting of values to determine the iteration order. That's no longer a concern for the standard library version, as iteration has been changed to use definition order.
msg189023 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2013-05-12 12:34
In flufl.enum integers also made since as that was the default back-end data type.

Currently, the functional method allows a type declaration.  The behavior there could be tweaked such that no specification meant no value (a truly valueless enum!), type=int means ints starting from 1, type=str meant str values of names, any other value and you better had made your own enum derived class to support it.
msg189067 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-05-12 22:00
On May 12, 2013, at 10:57 AM, Nick Coghlan wrote:

>I encountered an interesting suggestion [1] regarding the enum.Enum
>convenience API: use the member names as their values, rather than the
>current integers starting from one.
>
>Since we're now using definition order rather than value order for iteration,
>this suggestion makes a lot of sense to me.

Cute, but I want the functional API to be equivalent to the class syntax.

Besides:

X = Enum('X', ((name, name) for name in 'ant bee cat dog elk'.split()))

is not so much typing.
msg189072 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-05-12 23:24
That gives the unqualified names, and I don't understand your other
objection. The class syntax doesn't support implicit values at all.
msg189085 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2013-05-13 00:35
The class syntax (and default Enum) no longer have preferential treatment for integers (even __int__ is gone); so it is completely up to us as what should happen for the functional syntax.
msg189087 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-05-13 01:57
On May 12, 2013, at 8:35 PM, Ethan Furman wrote:

> Ethan Furman added the comment:
> 
> The class syntax (and default Enum) no longer have preferential treatment for integers (even __int__ is gone); so it is completely up to us as what should happen for the functional syntax.

Do you really think the enum discussion on python-dev was too short and could use another few thousand messages? ;)

Or IOW, hasn't this already been decided by virtue of PEP acceptance?
msg189088 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2013-05-13 02:06
On 05/12/2013 06:57 PM, Barry A. Warsaw wrote:
>
> Do you really think the enum discussion on python-dev was too short and could use another few thousand messages? ;)

No!

> Or IOW, hasn't this already been decided by virtue of PEP acceptance?

Indeed it has.  I was merely replying to your comment about keeping class and function syntax in sync, when in fact 
there is no longer any direct correlation between the two other than the names used and the type created.
msg189094 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-05-13 04:09
Accepting awkward implementation details when their rationale no longer
applies doesn't make sense to me, no.

The functional API originally used integers for good reasons. Over the
course of the enum discussions, those reasons evaporated, but this wasn't
noticed until after the PEP was accepted.

However, the acceptance of the PEP is why I have proposed this as a
post-incorporation change.
msg189115 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2013-05-13 10:10
I agree with Nick here, there's no reason to auto-number constants in Python. This is not C :-)
msg189132 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-05-13 14:01
On May 13, 2013, at 10:10 AM, Antoine Pitrou wrote:

>I agree with Nick here, there's no reason to auto-number constants in
>Python. This is not C :-)

Why should they be strings?   Why not object()?

Why is `x.value == str(x)` a useful invariant to hold?
msg189135 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2013-05-13 14:06
> >I agree with Nick here, there's no reason to auto-number constants
> >in
> >Python. This is not C :-)
> 
> Why should they be strings?   Why not object()?

Because strings are readable, I'd say.
msg189137 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-05-13 14:24
On 14 May 2013 00:06, "Antoine Pitrou" <report@bugs.python.org> wrote:
>
>
> Antoine Pitrou added the comment:
>
> > >I agree with Nick here, there's no reason to auto-number constants
> > >in
> > >Python. This is not C :-)
> >
> > Why should they be strings?   Why not object()?
>
> Because strings are readable, I'd say.

Yep. Since we no longer have a compelling reason for it to be anything
else, it may as well be the human readable string.
msg189140 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2013-05-13 14:44
So the repr will look like:

<Color.red: 'red'>

?
msg189142 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-05-13 15:09
On May 13, 2013, at 02:06 PM, Antoine Pitrou wrote:

>Antoine Pitrou added the comment:
>
>> >I agree with Nick here, there's no reason to auto-number constants
>> >in
>> >Python. This is not C :-)
>> 
>> Why should they be strings?   Why not object()?
>
>Because strings are readable, I'd say.

The repr would then be

<Color.red: Color.red>

Yuck.

Also, you would have to allow for subclasses (e.g. IntEnum) to override
auto-assignment.  Clearly, you can't use strings for

X = IntEnum('X', 'start middle end')
msg189143 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-05-13 15:10
On May 13, 2013, at 02:24 PM, Nick Coghlan wrote:

>Yep. Since we no longer have a compelling reason for it to be anything
>else, it may as well be the human readable string.

Again, why does it matter?  That's the whole point of having a human readable
str() and repr().
msg191449 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2013-06-19 06:45
Enum members have two pieces of easily accessible information: `name` and `value`.  The name is taken from the attribute the value is assigned to; the value, obviously, is the value assigned.

The question, then, is what should happen when the function syntax is used, and values are not explicitly given?

  - use `int`s starting from one

  - use `int`s starting from zero

  - use the key name itself

  - use no value at all

Working from the bottom up:

  - no value:  if the enum member has no actual value, the user cannot retrieve the member using the class call syntax (e.g. Color(???) )

  - the key name as value:  if we go this route, then instead of two pieces of info, our enum member has only one piece in two places

  - `int`s from zero:  falls in line with normal Python counting, but is the zeroth member False?

  - `int`s from one: avoids the False question, and it's what flufl.enum does.

No value is obviously a no-go as it causes more problems than it solves.

`str` value is also a no-go as the key name is already available as `name`.

`int`s starting from zero or starting from one?  Personally, I have no problem with a zero-value enum member that is True -- not every zero should be False.  This is the only change I would be willing to make.

To sum up:  the name is already available in the name, no need to have it be the value as well.  I am open to changing the start value to zero instead of one (which is what I do in my customization of Enum in my personal modules ;) .
msg191460 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-06-19 11:32
OK, I've satisfied myself that the current behaviour is reasonable, and it's specifically the subclassing behaviour of the status quo that works for me.

1. You have to specifically access the "x.value" attribute of a default enum member to see it. "str(x)" shows the fully qualified name, not the value (even for concrete subclasses).
2. The implicitly chosen values are valid for any concrete enum subclass that accepts a single integer as an argument. This covers strings and the whole numeric tower. The same can't be said for using the enum name.
3. The implicitly chosen values are always "true", even when used with a concrete numeric enum subclass. This matches the behaviour of standard user defined classes (where all instances are considered true by default unless you define __len__ or __bool__ to say otherwise).

The fact "str(x)" returns the fully qualified name for IntEnum worries me a bit, but if there's a real backwards compatibility problem there (rather than a theoretical one), hopefully we'll see it once we start converting socket and errno.
msg191462 - (view) Author: Eli Bendersky (eli.bendersky) * (Python committer) Date: 2013-06-19 12:56
Nicely done summary, Ethan.

I think it would be worthwhile to add the reasoning of "why from 1 and not from 0" into the documentation and maybe the PEP too. I think the "falsiness" answer is reasonable, and since it's a common FAQ it's good to state it explicitly. But make sure to specify that it only applies to IntEnum because Enum values are always truthy :)
msg191464 - (view) Author: Eli Bendersky (eli.bendersky) * (Python committer) Date: 2013-06-19 13:00
> The fact "str(x)" returns the fully qualified name for IntEnum worries me a bit, but if there's a real backwards compatibility problem there (rather than a theoretical one), hopefully we'll see it once we start converting socket and errno.


What is the theoretical problem here? I though that it's an explicit design goal of enums? Which RED - Color.RED, or MeatReadiness.RED? For sockets:

>>> class SocketType(IntEnum):
...   SOCK_STREAM = 1
...   SOCK_DGRAM = 2
... 
>>> str(SocketType.SOCK_STREAM)
'SocketType.SOCK_STREAM'

Looks pretty good to me in terms of debuggability.
msg191465 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-06-19 13:31
On Jun 19, 2013, at 01:00 PM, Eli Bendersky wrote:

>What is the theoretical problem here? I though that it's an explicit design
>goal of enums? Which RED - Color.RED, or MeatReadiness.RED? For sockets:
>
>>>> class SocketType(IntEnum):
>...   SOCK_STREAM = 1
>...   SOCK_DGRAM = 2
>... 
>>>> str(SocketType.SOCK_STREAM)
>'SocketType.SOCK_STREAM'
>
>Looks pretty good to me in terms of debuggability.

Me too.
msg191466 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-06-19 13:41
On Jun 19, 2013, at 06:45 AM, Ethan Furman wrote:

>To sum up: the name is already available in the name, no need to have it be
>the value as well.  I am open to changing the start value to zero instead of
>one (which is what I do in my customization of Enum in my personal modules ;)

I'm sure it's obvious that I prefer start-from-one, and aside from the
truthiness question, it would maximize compatibility with flufl.enum.
Besides, the functional API accepts a sequence so if you *really* wanted
start-from-zero, then you can do it easily.
msg191468 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2013-06-19 13:48
I created issue 18264 after I tried it and found my theoretical concern wasn't theoretical at all: swapping a true integer for the current incarnation of enum.IntEnum breaks (at least) JSON serialisation, which means we can't use it in its current form to replace stdlib constants.
msg191469 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2013-06-19 13:54
On Jun 19, 2013, at 01:48 PM, Nick Coghlan wrote:

>I created issue 18264 after I tried it and found my theoretical concern
>wasn't theoretical at all: swapping a true integer for the current
>incarnation of enum.IntEnum breaks (at least) JSON serialisation, which means
>we can't use it in its current form to replace stdlib constants.

JSON has to be taught how to serialize enums.  Of course, it also has to be
taught how to serialize datetimes, timedeltas, and other common data types.

In Mailman, I use the following subclass of json.JSONEncoder.  Note though
that in my REST API, I always know the class/enum that a string should be
associated with so my deserializer always does the right thing.

class ExtendedEncoder(json.JSONEncoder):
    """An extended JSON encoder which knows about other data types."""

    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        elif isinstance(obj, timedelta):
            # as_timedelta() does not recognize microseconds, so convert these
            # to floating seconds, but only if there are any seconds.
            if obj.seconds > 0 or obj.microseconds > 0:
                seconds = obj.seconds + obj.microseconds / 1000000.0
                return '{0}d{1}s'.format(obj.days, seconds)
            return '{0}d'.format(obj.days)
        elif isinstance(obj, Enum):
            # It's up to the decoding validator to associate this name with
            # the right Enum class.
            return obj.name
        return json.JSONEncoder.default(self, obj)
msg191472 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2013-06-19 14:07
> JSON has to be taught how to serialize enums.  Of course, it also has
> to be
> taught how to serialize datetimes, timedeltas, and other common data
> types.

How so? The point of IntEnum was that it derived from int, and therefore
would avoid any need for special-casing in C code.

(now if json has a PyLong_CheckExact() check, perhaps it can be relaxed
to PyLong_Check()...)
History
Date User Action Args
2013-06-19 14:07:53pitrousetmessages: + msg191472
2013-06-19 13:54:54barrysetmessages: + msg191469
2013-06-19 13:48:14ncoghlansetmessages: + msg191468
2013-06-19 13:41:29barrysetmessages: + msg191466
2013-06-19 13:31:29barrysetmessages: + msg191465
2013-06-19 13:00:52eli.benderskysetmessages: + msg191464
2013-06-19 12:56:12eli.benderskysetmessages: + msg191462
2013-06-19 11:32:02ncoghlansetstatus: open -> closed
resolution: rejected
messages: + msg191460

stage: needs patch -> resolved
2013-06-19 06:45:43ethan.furmansetnosy: + eli.bendersky
title: Use enum names as values in enum.Enum convenience API -> Use enum names as values in enum.Enum() functional API
messages: + msg191449

assignee: ethan.furman
2013-05-13 15:10:20barrysetmessages: + msg189143
2013-05-13 15:09:31barrysetmessages: + msg189142
2013-05-13 14:44:28ethan.furmansetmessages: + msg189140
2013-05-13 14:24:41ncoghlansetmessages: + msg189137
2013-05-13 14:06:28pitrousetmessages: + msg189135
2013-05-13 14:01:24barrysetmessages: + msg189132
2013-05-13 10:10:58pitrousetnosy: + pitrou
messages: + msg189115
2013-05-13 04:09:05ncoghlansetmessages: + msg189094
2013-05-13 02:06:04ethan.furmansetmessages: + msg189088
2013-05-13 01:57:08barrysetmessages: + msg189087
2013-05-13 00:35:53ethan.furmansetmessages: + msg189085
2013-05-12 23:24:21ncoghlansetmessages: + msg189072
2013-05-12 22:00:29barrysetmessages: + msg189067
2013-05-12 21:57:15barrysetnosy: + barry
2013-05-12 12:34:06ethan.furmansetmessages: + msg189023
2013-05-12 12:28:56ncoghlansetmessages: + msg189022
2013-05-12 11:12:31ethan.furmansetnosy: + ethan.furman
2013-05-12 10:57:35ncoghlansetdependencies: + Code, test, and doc review for PEP-0435 Enum
versions: + Python 3.4
2013-05-12 10:57:02ncoghlancreate