Enumerations

From IronPython Cookbook

Enumerations make source code easier to read because a name is shown instead of a number. It is easier to determine the meaning of ProcessType.FilesOnly than the meaning of 32. For a more explicit definition of enumerations please refer to Wikipedia.org.

Contents

Flagged Enumerations

Flagged enumerations are enumerations which have values such that any combination of enumeration values are valid. A complete explaination would be lengthy. See [1] for a great introduction to flagged enumerations (in VBScript) in the "Bitwise Operations" section.

Methods

A few methods of enumerations are:

Variable Method

class MyEnum:
	unknown = 0
	FOO = 1
	BAR = 2
	BAZ = 3

Range Method

class MyEnum:
	FOO, BAR, BAZ = range(3)

Class Method

Classes provide an easy and validated way of creating both flagged and unflagged enumerations. The IronPython engine translates .NET enumerations in a different manner because it does not allow adding enumeration types, but instead works more like a C# enum where a single pipe character is used to join two values.

An example of a class can be found below or at [2].

"""Enumeration Class

Class used to automatically handle flagged enumerations.  Based on the example at http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/67107

Version	Changes
1.0		Initial version
"""
__author__  = 'Dag R. Calafell, III'
__date__    = '2007-06-22'  # yyyy-mm-dd
__module_name__ = "Enumerations Class"
__short_cright__= " Creative Commons License" # http://creativecommons.org/licenses/by/3.0/
__version__ = '1.0'     # Human-Readable Version number
version_info = (1,0,0)  # Easier format: if version_info > (1,2,5)

from System import Array
from System.Text.RegularExpressions import Regex, RegexOptions

class EnumException(Exception):
	pass

class Enumeration:
	'''Allows for the definition of enumerations in Python.
	
	Volkswagen = Enumeration("Volkswagen",
		["Jetta",
		"Rabbit",
		"Beetle",
		("Thing", 400),
		"Passat",
		"Golf",
		("Cabrio", 700),
		"EuroVan",
		"ClassicBeetle",
		"ClassicVan"])
	'''
	def __init__(self, name, enumList, flagged = False):
		'''Initializes a new enumeration.
		
		name		The name of the enumeration.  It is assigned to the __doc__ attribute.
		enumList	The enumeration list.
		flagged		True if the values of the enumeration are able to be added.  All values
					of the enumeration will be exponents of 2, i.e. 2^0, 2^1, 2^2 ...
		
		returns		None
		'''
		if type(name) != str:
			raise ValueError, 'The name parameter must be a string value.'
		if type(enumList) not in (list, tuple, Array):
			raise ValueError, 'The name parameter must be a string value.'
		self.__doc__ = name
		self.__flagged = bool(flagged)
		self.__sum = 0 # Sum of all flagged enum values for faster self.IsValid processing.
		self.__uniqueValues = uniqueValues = [ ]
		self.__uniqueId = 0
		nameRegex = Regex('\\A[a-z][a-z0-9_]*\\z', RegexOptions.IgnoreCase | RegexOptions.Compiled)
		reservedNames = ('and','assert','break','class','continue','def','del','elif','else','except','exec','finally','for','from','global','if','import','in','is','lambda','not','or','pass','print','raise','return','try','while','yield')
		lookup = { }
		reverseLookup = { }
		uniqueNames = [ ]
		# Set the special values first
		for x in enumList:
			if type(x) == tuple:
				if self.__flagged:
					raise EnumException, 'A flagged enumeration cannot have a value which is asserted.'
				if len(x) != 2:
					raise EnumException, 'The enumeration cannot have more than one value.'
				x, i = x
				if type(x) != str or x in uniqueNames or not nameRegex.IsMatch(x) or x in reservedNames:
					raise EnumException, 'The enum name (%s) must be a unique string containing only letters, numbers, & underscores and it cannot be a reserved Python word.' % x
				if type(i) != int or i in uniqueValues:
					raise EnumException, 'The enum value (%s) must be a unique integer.' % i
				uniqueNames.append(x)
				uniqueValues.append(i)
				lookup[x] = i
				reverseLookup[i] = x
		# Now set the auto-assigned values
		for x in enumList:
			if type(x) != tuple:
				if type(x) != str or x in uniqueNames or not nameRegex.IsMatch(x) or x in reservedNames:
					raise EnumException, 'The enum name (%s) must be a unique string containing only letters, numbers, & underscores and it cannot be a reserved Python word.' % x
				uniqueNames.append(x)
				i = self.__genUniqueId()
				uniqueValues.append(i)
				self.__sum += i
				lookup[x] = i
				reverseLookup[i] = x
		self.lookup = lookup
		self.reverseLookup = reverseLookup

	def __genUniqueId(self):
		if self.__flagged:
			n = self.__uniqueId
			if self.__uniqueId == 0:
				self.__uniqueId = 1
			else:
				self.__uniqueId *= 2
		else:
			while self.__uniqueId in self.__uniqueValues:
				self.__uniqueId += 1
			n = self.__uniqueId
			self.__uniqueId += 1
		return n

	def __getattr__(self, attr):
		if attr not in self.lookup:
			raise AttributeError
		return self.lookup[attr]

	def __len__(self):
		return len(self.reverseLookup)

	def GetNames(self):
		return self.lookup

	def IsFlagged(self):
		return self.__flagged

	def IsValid(self, value):
		'''Determines if the supplied value is a valid value for this enumeration.'''
		if type(value) == int:
			if self.__flagged:
				#  It is valid as long as the value is less than or equal to the sum of all items
				return value <= self.__sum
			else:
				return value in self.reverseLookup
		elif type(value) == str:
			return value in self.lookup
		else:
			return False

	def NameOf(self, value):
		'''Determines the name of the specified value.
		
		value	Integer value of the enumeration.
		
		If the enumeration is flagged and the value is a combination of values then it will return None.'''
		if not self.IsValid(value):
			raise ValueError, 'The value is not valid.'
		return self.reverseLookup[value]

	def PrettyPrint(self):
		print self.__doc__
		print
		for v in self.lookup:
			print '%-15s %5s' % (v, getattr(self, v))

if __name__ == '__main__':
	Langs = Enumeration('Coding Languages',
		['Ruby',
		'Python',
		('PHP', 400),
		'Perl',
		'PostScript',
		('Java', 700),
		'VisualBasic',
		'IronPython'])
	print 'Testing'
	Langs.PrettyPrint()
	# Coding Languages
	# 
	# PHP               400
	# Java              700
	# Ruby                0
	# Python              1
	# Perl                2
	# PostScript          3
	# VisualBasic         4
	# IronPython          5
	print
	assert len(Langs) == 8, 'The __len__ method returned an incorrect value.'
	assert Langs.IsFlagged() == False, 'IsFlagged should be False, but the value was: %s' % Langs.IsFlagged()
	assert Langs.Ruby == Langs.Ruby, 'Langs.Ruby should equal Langs.Ruby.'
	assert Langs.PHP == Langs.PHP, 'Langs.PHP should equal Langs.PHP.'
	assert Langs.Java == 700, 'Langs.Java should have a value of 700.'
	assert Langs.IsValid(700) == True, 'The value 700 should be valid.  %s was returned' % Langs.IsValid(700)
	assert Langs.IsValid('IronPython'), 'Langs.IsValid(\'IronPython\') should return true.'
	assert Langs.IsValid(Langs.Perl), 'Langs.IsValid(Langs.Perl) should be return true.'
	assert Langs.NameOf(700) == 'Java', 'The name of the value 700 should be Java.  %s was returned.' % Langs.NameOf(700)
	assert Langs.NameOf(Langs.VisualBasic) == 'VisualBasic', 'The name of the value Langs.VisualBasic should be VisualBasic.  %s was returned.' % Langs.NameOf(Langs.VisualBasic)
	try:
		Langs.Php
		raise AssertionError('Lang.Php is not defined and should raise an AttributeError.')
	except AttributeError:
		pass
	try:
		Enumeration('Test', ['C#'])
		raise AssertionError('Enumeration initializer should verify that all enumeration names are valid Python names.')
	except EnumException:
		pass
	try:
		Enumeration('Test', ['One Space'])
		raise AssertionError('Enumeration initializer should verify that all enumeration names are valid Python names.')
	except EnumException:
		pass

	# Flagged enumerations
	myFlagged = Enumeration('School Classes This Semester', ['Math', 'Science', 'Gym', 'Cooking', 'Language'], True)
	print 'Testing'
	myFlagged.PrettyPrint()
	# School Classes This Semester
	# 
	# Math                0
	# Science             1
	# Gym                 2
	# Cooking             4
	# Language            8
	print
	assert len(myFlagged) == 5, 'The __len__ method returned an incorrect value.  %s was returned.' % len(myFlagged)
	assert myFlagged.IsFlagged() == True
	assert myFlagged.Gym == myFlagged.Gym
	assert myFlagged.Gym + myFlagged.Cooking != myFlagged.Gym
	assert myFlagged.IsValid(3)

Considerations

Performance
Variables are quickly accessed and allow compiler optimizations (if any).
Classes which override __getitem__ or __getattr__ are slower.
Bit-flags
Variables only support bit-flags when you assign the correct values (0, 1, 2, 4, 8, 16, 32, ...).
Classes can auto-assign values to members.
Ease of Use
Variable declaration can be time-consuming to maintain.
Why are you using Python? The obvious answer is that it is faster to code and prototype because it is not a strongly-typed language where each variable must be declared. Therefore why not have a class that makes all enumeration declarations faster and easier?
Classes can check that all values are unique and of the same data type.

Conclusions

There are many options when creating enumerations in [Iron]Python. What you choose should depend on what application is needed. If execution speed is a huge consideration, then the maintainance of a variable list seems trivial for the performance gained. If coding speed is most important then a class can simplify creating large enumerations.

Back to Contents.

TOOLBOX
LANGUAGES