Overhaul PEP 435 to describe the current state of decisions, which deviates

from flufl.enum:

1. type(Color.red) == Color
2. All attrs of an enum that are not dunders and are not descriptors become
   enumeration values
3. Given class Color(Enum), it's only valid to subclass Color if Color
   defines no enumeration values
4. As a corollary to (3), class IntEnum(int, Enum) is also part of the
   stdlib (and serves as an example for others)

Other issues that were contended recently but seem to have gotten resolved:

A. __call__ for by-value access, explicit getattr() for by-name access.
   __getitem__ is removed from Enum
B. __int__ is removed from Enum. It's present by definition in IntEnum

NOTE: this is still an early draft that is in the process of being reviewed,
so please treat it accordingly.
This commit is contained in:
Eli Bendersky 2013-05-02 05:51:33 -07:00
parent 801e07188f
commit d223705ffc
1 changed files with 204 additions and 255 deletions

View File

@ -3,22 +3,20 @@ Title: Adding an Enum type to the Python standard library
Version: $Revision$
Last-Modified: $Date$
Author: Barry Warsaw <barry@python.org>,
Eli Bendersky <eliben@gmail.com>
Eli Bendersky <eliben@gmail.com>,
Ethan Furman <ethan@stoneleaf.us>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 2013-02-23
Python-Version: 3.4
Post-History: 2013-02-23
Post-History: 2013-02-23, 2013-05-02
Abstract
========
This PEP proposes adding an enumeration type to the Python standard library.
Specifically, it proposes moving the existing ``flufl.enum`` package by Barry
Warsaw into the standard library. Much of this PEP is based on the "using"
[1]_ document from the documentation of ``flufl.enum``.
An enumeration is a set of symbolic names bound to unique, constant values.
Within an enumeration, the values can be compared by identity, and the
@ -28,7 +26,7 @@ enumeration itself can be iterated over.
Decision
========
TODO: update decision here once pronouncement is made.
TODO: update decision here once pronouncement is made. [1]_
Status of discussions
@ -55,9 +53,11 @@ interoperatiliby with integers is desired. For instance, this is the case for
replacing existing standard library constants (such as ``socket.AF_INET``)
with enumerations.
This PEP is an attempt to formalize this decision as well as discuss a number
of variations that were proposed and can be considered for inclusion.
Further discussion in late April 2013 led to the conclusion that enumeration
members should belong to the type of their enum: ``type(Color.red) == Color``.
Guido has pronounced a decision on this issue [5]_, as well as related issues
of not allowing to subclass enums [6]_, unless they define no enumeration
members [7]_.
Motivation
==========
@ -97,229 +97,151 @@ Creating an Enum
Enumerations are created using the class syntax, which makes them easy to read
and write. An alternative creation method is described in `Convenience API`_.
To define an enumeration, derive from the ``Enum`` class and add attributes
with assignment to their integer values::
To define an enumeration, subclass ``Enum`` as follows::
>>> from enum import Enum
>>> class Colors(Enum):
>>> class Color(Enum):
... red = 1
... green = 2
... blue = 3
Enumeration values have nice, human readable string representations::
**A note on nomenclature**: we call ``Color`` an *enumeration* (or *enum*)
and ``Color.red``, ``Color.green`` are *enumeration members* (or
*enum members*). Enumeration members also have *values* (the value of
``Color.red`` is ``1``, etc.)
>>> print(Colors.red)
Colors.red
Enumeration members have human readable string representations::
...while their repr has more information::
>>> print(Color.red)
Color.red
>>> print(repr(Colors.red))
<EnumValue: Colors.red [value=1]>
...while their ``repr`` has more information::
You can get the enumeration class object from an enumeration value::
>>> print(repr(Color.red))
Color.red [value=1]
>>> cls = Colors.red.enum
>>> print(cls.__name__)
Colors
The *type* of an enumeration member is the enumeration it belongs to::
>>> type(Color.red)
<Enum 'Color'>
>>> isinstance(Color.green, Color)
True
>>>
Enums also have a property that contains just their item name::
>>> print(Colors.red.name)
>>> print(Color.red.name)
red
>>> print(Colors.green.name)
green
>>> print(Colors.blue.name)
blue
The str and repr of the enumeration class also provides useful information::
Enumerations support iteration, in definition order::
>>> print(Colors)
<Colors {red: 1, green: 2, blue: 3}>
>>> print(repr(Colors))
<Colors {red: 1, green: 2, blue: 3}>
>>> class Shake(Enum):
... vanilla = 7
... chocolate = 4
... cookies = 9
... mint = 3
...
>>> for shake in Shake:
... print(shake)
...
Shake.vanilla
Shake.chocolate
Shake.cookies
Shake.mint
The ``Enum`` class supports iteration. Iteration order is undefined::
>>> from operator import attrgetter
>>> by_value = attrgetter('value')
>>> class FiveColors(Enum):
... pink = 4
... cyan = 5
... green = 2
... blue = 3
... red = 1
>>> [v.name for v in sorted(FiveColors, by_value)]
['red', 'green', 'blue', 'pink', 'cyan']
Iteration order over `IntEnum`_ enumerations are guaranteed to be
sorted by value.
>>> class Toppings(IntEnum):
... anchovies = 4
... black_olives = 8
... cheese = 2
... dried_tomatoes = 16
... eggplant = 1
>>> for value in Toppings:
... print(value.name, '=', value.value)
eggplant = 1
cheese = 2
anchovies = 4
black_olives = 8
dried_tomatoes = 16
Enumeration values are hashable, so they can be used in dictionaries and sets::
Enumeration members are hashable, so they can be used in dictionaries and sets::
>>> apples = {}
>>> apples[Colors.red] = 'red delicious'
>>> apples[Colors.green] = 'granny smith'
>>> apples[Color.red] = 'red delicious'
>>> apples[Color.green] = 'granny smith'
>>> apples
{<EnumValue: Colors.green [value=2]>: 'granny smith', <EnumValue: Colors.red [value=1]>: 'red delicious'}
{Color.red [value=1]: 'red delicious', Color.green [value=2]: 'granny smith'}
Programmatic access to enum values
----------------------------------
Programmatic access to enumeration members
------------------------------------------
Sometimes it's useful to access values in enumerations programmatically (i.e.
situations where ``Colors.red`` won't do because the exact color is not known
at program-writing time). ``Enum`` allows such access by value::
Sometimes it's useful to access members in enumerations programmatically (i.e.
situations where ``Color.red`` won't do because the exact color is not known
at program-writing time). ``Enum`` allows such access::
>>> Colors[1]
<EnumValue: Colors.red [value=1]>
>>> Colors[2]
<EnumValue: Colors.green [value=2]>
>>> Color(1)
Color.red [value=1]
>>> Color(3)
Color.blue [value=3]
For consistency, an ``EnumValue`` can be used in the same access pattern::
>>> Colors[Colors.red]
<EnumValue: Colors.red [value=1]>
>>> Colors[Colors.green]
<EnumValue: Colors.green [value=2]>
If you want to access enum values by *name*, ``Enum`` works as expected with
If you want to access enum members by *name*, ``Enum`` works as expected with
``getattr``::
>>> getattr(Colors, 'red')
<EnumValue: Colors.red [value=1]>
>>> getattr(Colors, 'green')
<EnumValue: Colors.green [value=2]>
>>> getattr(Color, 'red')
Color.red [value=1]
>>> getattr(Color, 'green')
Color.green [value=2]
Duplicating enum members and values
-----------------------------------
Having two enum members with the same name is invalid::
>>> class Shape(Enum):
... square = 2
... square = 3
...
Traceback (most recent call last):
...
TypeError: Attempted to reuse key: square
However, two enum members are allowed to have the same value. By-value lookup
will then access the *earliest defined* member::
>>> class Shape(Enum):
... square = 2
... diamond = 1
... circle = 3
... alias_for_square = 2
...
>>> Shape.square
Shape.square [value=2]
>>> Shape.alias_for_square
Shape.square [value=2]
>>> Shape(2)
Shape.square [value=2]
Comparisons
-----------
Enumeration values are compared by identity::
Enumeration members are compared by identity::
>>> Colors.red is Colors.red
>>> Color.red is Color.red
True
>>> Colors.blue is Colors.blue
True
>>> Colors.red is not Colors.blue
True
>>> Colors.blue is Colors.red
>>> Color.red is Color.blue
False
>>> Color.red is not Color.blue
True
Ordered comparisons between enumeration values are *not* supported. Enums are
not integers (but see `IntEnum`_ below)::
>>> Colors.red < Colors.blue
>>> Color.red < Color.blue
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.red <= Colors.blue
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.blue > Colors.green
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.blue >= Colors.green
Traceback (most recent call last):
...
NotImplementedError
File "<stdin>", line 1, in <module>
TypeError: unorderable types: Color() < Color()
Equality comparisons are defined though::
>>> Colors.blue == Colors.blue
True
>>> Colors.green != Colors.blue
>>> Color.blue == Color.red
False
>>> Color.blue == Color.blue
True
Comparisons against non-enumeration values will always compare not equal::
Comparisons against non-enumeration values will always compare not equal
(again, ``IntEnum`` was explicitly designed to behave differently, see
below)::
>>> Colors.green == 2
False
>>> Colors.blue == 3
False
>>> Colors.green != 3
True
>>> Colors.green == 'green'
>>> Color.blue == 2
False
Extending enumerations by subclassing
-------------------------------------
You can extend previously defined Enums by subclassing::
>>> class MoreColors(Colors):
... pink = 4
... cyan = 5
When extended in this way, the base enumeration's values are identical to the
same named values in the derived class::
>>> Colors.red is MoreColors.red
True
>>> Colors.blue is MoreColors.blue
True
However, these are not doing comparisons against the integer
equivalent values, because if you define an enumeration with similar
item names and integer values, they will not be identical::
>>> class OtherColors(Enum):
... red = 1
... blue = 2
... yellow = 3
>>> Colors.red is OtherColors.red
False
>>> Colors.blue is not OtherColors.blue
True
Because ``Colors`` and ``OtherColors`` are unrelated enumerations,
their values are not equal, and thus they may exist in the same set,
or as distinct keys in the same dictionary::
>>> Colors.red == OtherColors.red
False
>>> len(set((Colors.red, OtherColors.red)))
2
You may not define two enumeration values with the same integer value::
>>> class Bad(Enum):
... cartman = 1
... stan = 2
... kyle = 3
... kenny = 3 # Oops!
... butters = 4
Traceback (most recent call last):
...
ValueError: Conflicting enums with value '3': 'kenny' and 'kyle'
You also may not duplicate values in derived enumerations::
>>> class BadColors(Colors):
... yellow = 4
... chartreuse = 2 # Oops!
Traceback (most recent call last):
...
ValueError: Conflicting enums with value '2': 'green' and 'chartreuse'
Enumeration values
------------------
Allowed members and attributs of enumerations
---------------------------------------------
The examples above use integers for enumeration values. Using integers is
short and handy (and provided by default by the `Convenience API`_), but not
@ -332,7 +254,7 @@ enumerations can have arbitrary values. The following example uses strings::
... adaptor = '~$IM'
...
>>> SpecialId.selector
<EnumValue: SpecialId.selector [value=$IM($N)]>
SpecialId.selector [value='$IM($N)']
>>> SpecialId.selector.value
'$IM($N)'
>>> a = SpecialId.adaptor
@ -342,8 +264,6 @@ enumerations can have arbitrary values. The following example uses strings::
True
>>> print(a)
SpecialId.adaptor
>>> print(a.value)
~$IM
Here ``Enum`` is used to provide readable (and syntactically valid!) names for
some special values, as well as group them together.
@ -353,14 +273,73 @@ very special cases. Code will be most readable when actual values of
enumerations aren't important and enumerations are just used for their
naming and comparison properties.
Enumerations are Python classes, and can have methods and special methods as
usual. If we have this enumeration::
class Mood(Enum):
funky = 1
happy = 3
def describe(self):
# self is the member here
return self.name, self.value
def __str__(self):
return 'my custom str! {0}'.format(self.value)
@classmethod
def favorite_mood(cls):
# cls here is the enumeration
return cls.happy
Then::
>>> Mood.favorite_mood()
Mood.happy [value=3]
>>> Mood.happy.describe()
('happy', 3)
>>> str(Mood.funky)
'my custom str! 1'
The rules for what is allowed are as follows: all attributes defined within an
enumeration will become members of this enumeration, with the exception of
*__dunder__* names and descriptors; methods are descriptors too.
Restricted subclassing of enumerations
--------------------------------------
Subclassing an enumeration is allowed only if the enumeration does not define
any members. So this is forbidden::
>>> class MoreColor(Color):
... pink = 17
...
TypeError: Cannot subclass enumerations
But this is allowed::
>>> class Foo(Enum):
... def some_behavior(self):
... pass
...
>>> class Bar(Foo):
... happy = 1
... sad = 2
...
The rationale for this decision was given by Guido in [6]_. Allowing to
subclass enums that define members would lead to a violation of some
important invariants of types and instances. On the other hand, it
makes sense to allow sharing some common behavior between a group of
enumerations, and subclassing empty enumerations is also used to implement
``IntEnum``.
IntEnum
-------
A variation of ``Enum`` is proposed where the enumeration values also
subclasses ``int`` - ``IntEnum``. These values can be compared to
integers; by extension, enumerations of different types can also be
compared to each other::
A variation of ``Enum`` is proposed which is also a subclass of ``int``.
Members of an ``IntEnum`` can be compared to integers; by extension,
integer enumerations of different types can also be compared to each other::
>>> from enum import IntEnum
>>> class Shape(IntEnum):
@ -384,11 +363,11 @@ However they still can't be compared to ``Enum``::
... circle = 1
... square = 2
...
>>> class Colors(Enum):
>>> class Color(Enum):
... red = 1
... green = 2
...
>>> Shape.circle == Colors.red
>>> Shape.circle == Color.red
False
``IntEnum`` values behave like integers in other ways you'd expect::
@ -425,21 +404,23 @@ Convenience API
The ``Enum`` class is callable, providing the following convenience API::
>>> Animals = Enum('Animals', 'ant bee cat dog')
>>> Animals
<Animals {ant: 1, bee: 2, cat: 3, dog: 4}>
>>> Animals.ant
<EnumValue: Animals.ant [value=1]>
>>> Animals.ant.value
>>> Animal = Enum('Animal', 'ant bee cat dog')
>>> Animal
<Enum 'Animal'>
>>> Animal.ant
Animal.ant [value=1]
>>> Animal.ant.value
1
>>> list(Animal)
[Animal.ant [value=1], Animal.bee [value=2], Animal.cat [value=3], Animal.dog [value=4]]
The semantics of this API resemble ``namedtuple``. The first argument of
the call to ``Enum`` is the name of the enumeration. The second argument is
a source of enumeration value names. It can be a whitespace-separated string
a source of enumeration member names. It can be a whitespace-separated string
of names, a sequence of names or a sequence of 2-tuples with key/value pairs.
The last option enables assigning arbitrary values to enumerations; the others
auto-assign increasing integers starting with 1. A new class derived from
``Enum`` is returned. In other words, the above assignment to ``Animals`` is
``Enum`` is returned. In other words, the above assignment to ``Animal`` is
equivalent to::
>>> class Animals(Enum):
@ -448,26 +429,22 @@ equivalent to::
... cat = 3
... dog = 4
Examples of alternative name/value specifications::
>>> Enum('Animals', ['ant', 'bee', 'cat', 'dog'])
<Animals {ant: 1, bee: 2, cat: 3, dog: 4}>
>>> Enum('Animals', (('ant', 'one'), ('bee', 'two'), ('cat', 'three'), ('dog', 'four')))
<Animals {dog: four, ant: one, cat: three, bee: two}>
The second argument can also be a dictionary mapping names to values::
>>> levels = dict(debug=10, info=20, warning=30, severe=40)
>>> Enum('Levels', levels)
<Levels {debug: 10, info: 20, warning: 30, severe: 40}>
Proposed variations
===================
Some variations were proposed during the discussions in the mailing list.
Here's some of the more popular ones.
flufl.enum
----------
``flufl.enum`` was the reference implementation upon which this PEP was
originally based. Eventually, it was decided against the inclusion of
``flufl.enum`` because its design separated enumeration members from
enumerations, so the former are not instances of the latter. Its design
also explicitly permits subclassing enumerations for extending them with
more members (due to the member/enum separation, the type invariants are not
violated in ``flufl.enum`` with such a scheme).
Not having to specify values for enums
--------------------------------------
@ -487,7 +464,6 @@ Cons: involves much magic in the implementation, which makes even the
definition of such enums baffling when first seen. Besides, explicit is
better than implicit.
Using special names or forms to auto-assign enum values
-------------------------------------------------------
@ -519,7 +495,6 @@ extend it, especially for large enumerations.
Cons: actually longer to type in many simple cases. The argument of explicit
vs. implicit applies here as well.
Use-cases in the standard library
=================================
@ -553,51 +528,24 @@ cases for replacing internal state constants with enums. The same can be said
about a lot of networking code (especially implementation of protocols) and
can be seen in test protocols written with the Tulip library as well.
Differences from PEP 354
========================
Unlike PEP 354, enumeration values are not defined as a sequence of strings,
but as attributes of a class. This design was chosen because it was felt that
class syntax is more readable.
Unlike PEP 354, enumeration values require an explicit integer value. This
difference recognizes that enumerations often represent real-world values, or
must interoperate with external real-world systems. For example, to store an
enumeration in a database, it is better to convert it to an integer on the way
in and back to an enumeration on the way out. Providing an integer value also
provides an explicit ordering. However, there is no automatic conversion to
and from the integer values, because explicit is better than implicit.
Unlike PEP 354, this implementation does use a metaclass to define the
enumeration's syntax, and allows for extended base-enumerations so that the
common values in derived classes are identical (a singleton model). While PEP
354 dismisses this approach for its complexity, in practice any perceived
complexity, though minimal, is hidden from users of the enumeration.
Unlike PEP 354, enumeration values should only be tested by identity
comparison. This is to emphasize the fact that enumeration values are
singletons, much like ``None``.
Acknowledgments
===============
This PEP describes the ``flufl.enum`` package by Barry Warsaw. ``flufl.enum``
is based on an example by Jeremy Hylton. It has been modified and extended
by Barry Warsaw for use in the GNU Mailman [5]_ project. Ben Finney is the
author of the earlier enumeration PEP 354.
This PEP was initially proposing including the ``flufl.enum`` package [8]_
by Barry Warsaw into the stdlib, and is inspired in large parts by it.
Ben Finney is the author of the earlier enumeration PEP 354.
References
==========
.. [1] http://pythonhosted.org/flufl.enum/docs/using.html
.. [1] Placeholder for pronouncement
.. [2] http://www.python.org/dev/peps/pep-0354/
.. [3] http://mail.python.org/pipermail/python-ideas/2013-January/019003.html
.. [4] http://mail.python.org/pipermail/python-ideas/2013-February/019373.html
.. [5] http://www.list.org
.. [5] http://mail.python.org/pipermail/python-dev/2013-April/125687.html
.. [6] http://mail.python.org/pipermail/python-dev/2013-April/125716.html
.. [7] http://mail.python.org/pipermail/python-dev/2013-May/125859.html
.. [8] http://pythonhosted.org/flufl.enum/
Copyright
=========
@ -608,7 +556,8 @@ This document has been placed in the public domain.
Todo
====
* Mark PEP 354 "superseded by" this one, if accepted
* Mark PEP 354 "superseded by" this one, if accepted
* The last revision where flufl.enum was the approach is cb3c18a080a3
..
Local Variables: