Fork me on GitHub

A screencast overview of python enums as introduced by PEP 435

Sat 28 September 2013 | tags: python

Earlier in the week, I made my first screencast to give an overview of PEP 435 which introduces enums to the python standard library.

 

The code below is the set of python notes I had prepared as I made the screencast.

# What are enumerations ?
# A series of immutable discrete arbitrary values
# of some type
# may or may not have a semantic meaning

# Why need them as a separate construct ?

# You can represent them as constants with values eg. ints
# But what does GREEN * 2 mean ?
# And if GREEN = 2, when you print / log GREEN, you expect to see GREEN not 2
# And sometimes you want to treat all the values as a set eg. days of week

# PEP 435 is the formal python document introducing enums
# Implemented in 3.4. You will need a python 3.4 runtime

# How are enumerations constructed ?

from enum import Enum

# There are two ways to construct enums. The first is a functional API. eg.

Direction = Enum('Direction','North East South West')

# Direction is an enumeration
# It has 4 members

list(Direction)

# Each member has a name and an associated value
# This is more apparent when you render the __repr__ of the member

list(Direction)[0]

# in this case the values were automatically provided.
# When values are automatically generated they are integers starting from 1

# You could also pass the member name and value as 2 tuples
StrDirection = Enum ('StrDirection',(("North", "N"),("East","E"), ("West", "W"), ("South", "S")))
list(StrDirection)

# Or as a dict
AngleDirection = Enum ('AngleDirection',{"North": 90,"East":0, "West": 270, "South": 180})
list(AngleDirection)

# Note: while Direction is an iterable in definition order,
#       you can't pick up the zero'th element eg. Direction[0]
#       ie. you cannot index by position
# However Direction.North and Direction["North"] are both valid

Direction.North
Direction["North"]

# Curiously a enum can also be looked up using a value by using the call operator

Direction(1)

# A key property of an enum is that its members are hashable

# Internal structure.
# Every Enum type has a __members__ attribute of the type OrderedDict

Direction.__members__
list(Direction.__members__.keys())
list(Direction.__members__.values())
list(Direction.__members__.items())

# enums are compared by identity and can be checked for equality
# however you cannot perform ordered comparisons

#   Aliases are not returned when iterating
# While two members cannot share the same name, they can share the same value (aliases)
# There is only one member that is shared in case of aliases

print(list(name for name, member in Colour.__members__.items() if member.name != name))

# An alternative way of defining an enum is a class
# Enum classes can have instance and classmethods

class Timezone:
    def __init__(self,offset,dst) :
        self.offset = offset
        self.dst = dst
    def earlier_than(self, other) :
        # ignoring dst
        return self.offset < other.offset
    def __eq__(self,other) :
        return self.offset == other.offset and self.dst == other.dst
    def __str__(self) :
        return "TZ({},{})".format(self.offset, self.dst)
    def __repr__(self) :
        return "TZ({},{})".format(self.offset, self.dst)

class Timezones(Enum):
    IST = Timezone(330, False)
    GMT = Timezone(0, False)
    UTC = Timezone(0, False)
    EST = Timezone(-300, False)
    @classmethod
    def all_zones(cls):
        #   Aliases are not returned when iterating
        return list(n.name for n in cls)
    @classmethod
    def aliases(cls):
        return [(name, member.name) for name, member in
                cls.__members__.items() if  member.name != name]
    def offset(self) :
        return  self.value.offset


for tz in Timezones :
    print(tz)

Timezones.all_zones()
Timezones.IST.offset()

list(Timezones)
list(Timezones.__members__.keys())
list(Timezones.__members__.values())
id(Timezones.GMT)
id(Timezones.UTC)

Timezones.IST == Timezones.GMT

Timezones.GMT == Timezones.IST

Timezone.UTC < Timezone.IST

# An enum which does not have any members can be subclassed. One which has cannot be

# So far the values per se have not been important (except to distinguish between distinct / aliased members)
# However in some cases it is important, and for that a special case Enum ie. IntEnum is defined

from enum import IntEnum

class Education(IntEnum):
    Kindergarten = 1
    Primary = 2
    Secondary = 3
    HigherSecondary = 4
    Graduate = 5 # elsewhere eg. US, this would be under graduate
    PostGraduate = 6 # and this would be called graduate
    Doctorate = 7

# This is an example where the value (and its ordering particularly is considered important)
# Now operators relevant to the semantics of the mixed in type (in this case int) can be used

Education.Secondary > Education.Primary

# Note that this opens up semantic issues of equivalence across enum types. eg.

class Colour(IntEnum):
    Red = 1
    Green = 2
    Blue = 3

Education.Secondary == Colour.Blue

# Thus you can choose to bring in any value types consistent with their appropriate semantics
# Note that this could lead to unexpected results eg.

list(range(Education.Graduate))

# However IntEnum is just a special case of being able to mix in values of any types ie.

class IntEnum(int, Enum) :
    pass

# So use value types that are semantically consistent

Comments !

social