Updated PEP 435:

* split the main section to some subsections, to make it more readable
* described non-int values
* removed mentions of implicit int-ness of Enum, which is no longer
  supported (unless in IntEnum)
* described the new convenience API
* misc. cleanups
This commit is contained in:
Eli Bendersky 2013-04-04 06:52:14 -07:00
parent cb3c841bab
commit 44b674e3a4
1 changed files with 179 additions and 165 deletions

View File

@ -20,11 +20,9 @@ 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" Warsaw into the standard library. Much of this PEP is based on the "using"
[1]_ document from the documentation of ``flufl.enum``. [1]_ document from the documentation of ``flufl.enum``.
An enumeration is a set of symbolic names bound to unique, constant integer An enumeration is a set of symbolic names bound to unique, constant values.
values. Within an enumeration, the values can be compared by identity, and the Within an enumeration, the values can be compared by identity, and the
enumeration itself can be iterated over. Enumeration items can be converted to enumeration itself can be iterated over.
and from their integer equivalents, supporting use cases such as storing
enumeration values in a database.
Decision Decision
@ -109,17 +107,6 @@ with assignment to their integer values::
... green = 2 ... green = 2
... blue = 3 ... blue = 3
Enumeration values are compared by identity::
>>> Colors.red is Colors.red
True
>>> Colors.blue is Colors.blue
True
>>> Colors.red is not Colors.blue
True
>>> Colors.blue is Colors.red
False
Enumeration values have nice, human readable string representations:: Enumeration values have nice, human readable string representations::
>>> print(Colors.red) >>> print(Colors.red)
@ -161,6 +148,107 @@ The str and repr of the enumeration class also provides useful information::
>>> print(repr(Colors)) >>> print(repr(Colors))
<Colors {red: 1, green: 2, blue: 3}> <Colors {red: 1, green: 2, blue: 3}>
The ``Enum`` class supports iteration. The returned order is not guaranteed
(unless you use `IntEnum`_)::
>>> [v.name for v in MoreColors]
['red', 'green', 'blue', 'pink', 'cyan']
Enumeration values are hashable, so they can be used in dictionaries and sets::
>>> apples = {}
>>> apples[Colors.red] = 'red delicious'
>>> apples[Colors.green] = 'granny smith'
>>> apples
{<EnumValue: Colors.green [value=2]>: 'granny smith', <EnumValue: Colors.red [value=1]>: 'red delicious'}
You can convert back to the enumeration value by indexing into the
``Enum`` subclass, passing in the integer value for the item you want::
>>> Colors[1]
<EnumValue: Colors.red [int=1]>
>>> Colors[2]
<EnumValue: Colors.green [int=2]>
>>> Colors[3]
<EnumValue: Colors.blue [int=3]>
>>> Colors[1] is Colors.red
True
The string name of the enumeration value is also accepted::
>>> Colors['red']
<EnumValue: Colors.red [int=1]>
>>> Colors['blue'] is Colors.blue
True
You get exceptions though, if you try to use invalid arguments::
>>> Colors['magenta']
Traceback (most recent call last):
...
ValueError: magenta
>>> Colors[99]
Traceback (most recent call last):
...
ValueError: 99
Comparisons
-----------
Enumeration values are compared by identity::
>>> Colors.red is Colors.red
True
>>> Colors.blue is Colors.blue
True
>>> Colors.red is not Colors.blue
True
>>> Colors.blue is Colors.red
False
Ordered comparisons between enumeration values are *not* supported. Enums are
not integers (but see `IntEnum`_ below)::
>>> Colors.red < Colors.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
Equality comparisons are defined though::
>>> Colors.blue == Colors.blue
True
>>> Colors.green != Colors.blue
True
Comparisons against non-enumeration values will always compare not equal::
>>> Colors.green == 2
False
>>> Colors.blue == 3
False
>>> Colors.green != 3
True
>>> Colors.green == 'green'
False
Extending enumerations by subclassing
-------------------------------------
You can extend previously defined Enums by subclassing:: You can extend previously defined Enums by subclassing::
>>> class MoreColors(Colors): >>> class MoreColors(Colors):
@ -195,112 +283,7 @@ These enumeration values are not equal, nor do they hash equally::
>>> len(set((Colors.red, OtherColors.red))) >>> len(set((Colors.red, OtherColors.red)))
2 2
Ordered comparisons between enumeration values are *not* supported. Enums are You may not define two enumeration values with the same integer value::
not integers (but see ``IntEnum`` below)::
>>> Colors.red < Colors.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
Equality comparisons are defined though::
>>> Colors.blue == Colors.blue
True
>>> Colors.green != Colors.blue
True
Enumeration values do not support ordered comparisons::
>>> Colors.red < Colors.blue
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.red < 3
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.red <= 3
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.blue > 2
Traceback (most recent call last):
...
NotImplementedError
>>> Colors.blue >= 2
Traceback (most recent call last):
...
NotImplementedError
While equality comparisons are allowed, comparisons against non-enumeration
values will always compare not equal::
>>> Colors.green == 2
False
>>> Colors.blue == 3
False
>>> Colors.green != 3
True
>>> Colors.green == 'green'
False
If you really want the integer equivalent values, you can convert enumeration
values explicitly using the ``int()`` built-in. This is quite convenient for
storing enums in a database, as well as for interoperability with C extensions
that expect integers::
>>> int(Colors.red)
1
>>> int(Colors.green)
2
>>> int(Colors.blue)
3
You can also convert back to the enumeration value by indexing into the Enum
subclass, passing in the integer value for the item you want::
>>> Colors[1]
<EnumValue: Colors.red [int=1]>
>>> Colors[2]
<EnumValue: Colors.green [int=2]>
>>> Colors[3]
<EnumValue: Colors.blue [int=3]>
>>> Colors[1] is Colors.red
True
The string name of the enumeration value is also accepted::
>>> Colors['red']
<EnumValue: Colors.red [int=1]>
>>> Colors['blue'] is Colors.blue
True
You get exceptions though, if you try to use invalid arguments::
>>> Colors['magenta']
Traceback (most recent call last):
...
ValueError: magenta
>>> Colors[99]
Traceback (most recent call last):
...
ValueError: 99
The integer equivalent values serve another purpose. You may not define two
enumeration values with the same integer value::
>>> class Bad(Enum): >>> class Bad(Enum):
... cartman = 1 ... cartman = 1
@ -321,23 +304,42 @@ You also may not duplicate values in derived enumerations::
... ...
TypeError: Multiple enum values: 2 TypeError: Multiple enum values: 2
The Enum class support iteration. Enumeration values are returned in the
sorted order of their integer equivalent values::
>>> [v.name for v in MoreColors] Enumeration values
['red', 'green', 'blue', 'pink', 'cyan'] ------------------
>>> [int(v) for v in MoreColors]
[1, 2, 3, 4, 5]
Enumeration values are hashable, so they can be used in dictionaries and sets:: The examples above use integers for enumeration values. Using integers is
short and handy (and provided by default by the `Convenience API`_), but not
strictly enforced. In the vast majority of use-cases, one doesn't care what
the actual value of an enumeration is. But if the value *is* important,
enumerations can have arbitrary values. The following example uses strings::
>>> class SpecialId(Enum):
... selector = '$IM($N)'
... adaptor = '~$IM'
...
>>> SpecialId.selector
<EnumValue: SpecialId.selector [value=$IM($N)]>
>>> SpecialId.selector.value
'$IM($N)'
>>> a = SpecialId.adaptor
>>> a == '~$IM'
False
>>> a == SpecialId.adaptor
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.
While ``Enum`` supports this flexibility, one should only use it in 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.
>>> apples = {}
>>> apples[Colors.red] = 'red delicious'
>>> apples[Colors.green] = 'granny smith'
>>> for color in sorted(apples, key=int):
... print(color.name, '->', apples[color])
red -> red delicious
green -> granny smith
IntEnum IntEnum
------- -------
@ -377,6 +379,15 @@ However they still can't be compared to ``Enum``::
>>> Shape.circle == Colors.red >>> Shape.circle == Colors.red
False False
``IntEnum`` behaves like an integer in other ways you'd expect::
>>> int(Shape.circle)
1
>>> ['a', 'b', 'c'][Shape.circle]
'b'
>>> [i for i in range(Shape.square)]
[0, 1]
For the vast majority of code, ``Enum`` is strongly recommended. Since For the vast majority of code, ``Enum`` is strongly recommended. Since
``IntEnum`` breaks some semantic promises of an enumeration (by being comparable ``IntEnum`` breaks some semantic promises of an enumeration (by being comparable
to integers, and thus by transitivity to other unrelated enumerations), it to integers, and thus by transitivity to other unrelated enumerations), it
@ -384,6 +395,7 @@ should be used only in special cases where there's no other choice; for example,
when integer constants are replaced with enumerations and backwards when integer constants are replaced with enumerations and backwards
compatibility is required with code that still expects integers. compatibility is required with code that still expects integers.
Pickling Pickling
-------- --------
@ -394,36 +406,42 @@ Enumerations created with the class syntax can also be pickled and unpickled::
>>> Fruit.tomato is loads(dumps(Fruit.tomato)) >>> Fruit.tomato is loads(dumps(Fruit.tomato))
True True
Convenience API Convenience API
--------------- ---------------
TODO: update to the new convenience API The ``Enum`` class is callable, providing the following convenience API::
You can also create enumerations using the convenience function ``make()``, >>> Animals = Enum('Animals', 'ant bee cat dog')
which takes an iterable object or dictionary to provide the item names and >>> Animals
values. ``make()`` is a module-level function.
The first argument to ``make()`` is the name of the enumeration, and it returns
the so-named `Enum` subclass. The second argument is a *source* which can be
either an iterable or a dictionary. In the most basic usage, *source* returns
a sequence of strings which name the enumeration items. In this case, the
values are automatically assigned starting from 1::
>>> import enum
>>> enum.make('Animals', ('ant', 'bee', 'cat', 'dog'))
<Animals {ant: 1, bee: 2, cat: 3, dog: 4}> <Animals {ant: 1, bee: 2, cat: 3, dog: 4}>
>>>
>>> Animals.ant
<EnumValue: Animals.ant [value=1]>
>>> Animals.ant.value
1
The items in source can also be 2-tuples, where the first item is the The semantics of this API resemble ``namedtuple``. The first argument of
enumeration value name and the second is the integer value to assign to the the call to ``Enum`` is the name of the enumeration. The second argument is
value. If 2-tuples are used, all items must be 2-tuples:: a source of enumeration value 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
equivalent to::
>>> def enumiter(): >>> class Animals(Enum):
... start = 1 ... ant = 1
... while True: ... bee = 2
... yield start ... cat = 3
... start <<= 1 ... dog = 4
>>> enum.make('Flags', zip(list('abcdefg'), enumiter()))
<Flags {a: 1, b: 2, c: 4, d: 8, e: 16, f: 32, g: 64}> 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}>
Proposed variations Proposed variations
@ -432,6 +450,7 @@ Proposed variations
Some variations were proposed during the discussions in the mailing list. Some variations were proposed during the discussions in the mailing list.
Here's some of the more popular ones. Here's some of the more popular ones.
Not having to specify values for enums Not having to specify values for enums
-------------------------------------- --------------------------------------
@ -450,6 +469,7 @@ Cons: involves much magic in the implementation, which makes even the
definition of such enums baffling when first seen. Besides, explicit is definition of such enums baffling when first seen. Besides, explicit is
better than implicit. better than implicit.
Using special names or forms to auto-assign enum values Using special names or forms to auto-assign enum values
------------------------------------------------------- -------------------------------------------------------
@ -571,12 +591,6 @@ Todo
==== ====
* Mark PEP 354 "superseded by" this one, if accepted * Mark PEP 354 "superseded by" this one, if accepted
* New package name within stdlib - enum? (top-level)
* For make, can we add an API like namedtuple's?
make('Animals, 'ant bee cat dog')
I.e. when make sees a string argument it splits it, making it similar to a
tuple but with far less manual quote typing. OTOH, it just saves a ".split"
so may not be worth the effort ?
.. ..
Local Variables: Local Variables: