Python GUI 038 – Transpo Groundwork 7 – Enum

I’ve got the autosolvers working for Cadenus, Railfence and Route Transposition. I threw in a wordlist solver method for Cadenus because things get real slow when going from message lengths of 100 to 125 and 150 characters (Cadenus message lengths have to be a factor of 25), but I don’t have hillclimbers for any of them because they’re not suited to hillclimbing.

I was going to start by documenting the Route solver first, but I got to thinking about the issue of enums, and this made me wonder if there are speed differences to the different approaches. I decided to hold off on the Route blog entry until I could do some testing, and what I found surprised me. It also means I have to do some optimizing of my code.

First, what are enums?

Enumeration is the process of turning something into a numeric value. The easiest example would be to assign numbers to constants of a particular group of items, such as colors.

RED = 0
BLUE = 1
YELLOW = 2

Now, if we want to test whether a color variable is a specific value, we could do something like:

color = RED
print_name(color)

def print_name(c):
... if c == RED: print('Red')
... elif c == BLUE: print('Blue')
... elif c == YELLOW: print('Yellow')

It may be obvious that this is not a great approach. For one thing, it’s going to get very messy if we have thousands of different colors, and we want to insert a new value between RED and BLUE (i.e. – GREEN = 1, then BLUE needs to be bumped to 2, etc.) Additionally, we’d need to preface each constant name with something like COLOR_SPACE_ if we want to identify one set of enums from every other set.

This is where the Python enum class comes in.

class Colors(Enum):
... RED = 0
... BLUE = 1
... YELLOW = 2

Now, we can select our colors this way:

color = Colors.RED
print_name(color)

def print_name(c):
... if c == Colors.RED: print('Red')
... elif c == Colors.BLUE: print('Blue')
... elif c == Colors.YELLOW: print('Yellow')

This is the approach I’d been using in the earlier versions of my route solver, but I was surprised to find it didn’t work when I got to the GUIs, specifically the display_entry() function.

entries['con_no'].set(record[1][ConTitleFields.con_no.value])
entries['type'].set(record[1][ConTitleFields.con_type.value])
entries['title'].set(record[1][ConTitleFields.title.value])

ConTitleFields is an enum, but I was forced to add that “.value” part to each one. It took me a really long time to realize what was happening.

I’m going to switch to the enum I’m using for the route solver.

class OnBy(Enum):
... ROWS = 0
... COLS = 1
... ALTROWS = 2
... ALTCOLS = 3
... DIAGS = 4
... ALTDIAGS = 5
... REVDIAGS = 6
... ALTREVDIAGS = 7
... SPIRAL = 8
... CCWSPIRAL = 9

Look what happens when I print the ROWS enum name and its value:

print(OnBy.ROWS) ..... -> OnBy.ROWS
print(OnBy.ROWS.value) -> 0

OnBy.ROWS is an element of type OnBy, whereas its value is the numeric 0. This explains why ConTitleFields.con_no didn’t work in the above GUI sample – I’m trying to use a non-numeric value as a list index selector.

One last choice before I get into testing.
Enums look a lot like dict objects. But, rather than using constants as dict keys, I’m going to use strings. And I’m going to create my “dict as enum” in a function to keep this out of the main() body.

def get_onby():
... ret = {'ROWS': 0, 'COLS': 1, 'ALTROWS': 2, 'ALTCOLS': 3, \

.......... 'DIAGS': 4, 'ALTDIAGS': 5, 'REVDIAGS': 6, \
.......... 'ALTREVDIAGS': 7, 'SPIRAL': 8, 'CCWSPIRAL': 9}
... return ret

onby_dict = get_onby()
onby = onby_dict['ROWS']

print(onby) -> 0

if onby == onby_dict['ROWS']:
... print('Rows')

Ok, testing time. I need to make the playing field as level as I can, which means I’m going to run through each element in each version of the enum approach, and call a test function filled with if/elif for decision branching. This simulates the actual solver code, which builds up the cipher message as on-by rows, by columns, etc., one route pattern at a time. I’m going to loop each approach 1 million times, and repeat that three times each, to flatten out any variations in CPU runtimes caused by background Windows crap. And, I’m going to do everything in sequence in one program run.

Also, I decided that for completeness sake I’d throw in the individual global ints just to try to get a baseline for the run times.

from enum import Enum
from time import time

class OnBy(Enum):
... ROWS = 0
... COLS = 1
... ALTROWS = 2
... ALTCOLS = 3
... DIAGS = 4
... ALTDIAGS = 5
... REVDIAGS = 6
... ALTREVDIAGS = 7
... SPIRAL = 8
... CCWSPIRAL = 9

ROWS = 0
COLS = 1
ALTROWS = 2
ALTCOLS = 3
DIAGS = 4
ALTDIAGS = 5
REVDIAGS = 6
ALTREVDIAGS = 7
SPIRAL = 8
CCWSPIRAL = 9

def main():

... onby = get_onby()

... range1 = 10000
... range2 = 10000

... start_time = time()
... for i in range(range1): # Global int vars
....... for j in range(range2):
........... for e in range(10):
............... a = test_0(e)
... print('Test 0 time: %s' % (time() - start_time))

... start_time = time()
... for i in range(range1): # Enum by name
....... for j in range(range2):
........... for e in OnBy:
............... a = test_1(e)
... print('Test 1 time: %s' % (time() - start_time))

... start_time = time()
... for i in range(range1): # Enum by value
....... for j in range(range2):
........... for e in OnBy:
............... a = test_2(e.value)
... print('Test 2 time: %s' % (time() - start_time))

... start_time = time()
... for i in range(range1): # dict
....... for j in range(range2):
........... for e in onby:
............... a = test_3(e)
... print('Test 3 time: %s' % (time() - start_time))

def test_0(e): # Global int vars
... a = 0
... if e == ROWS:
....... a = 1
... elif e == COLS:
....... a = 2
... elif e == ALTROWS:
....... a = 3
... elif e == ALTCOLS:
....... a = 4
... elif e == DIAGS:
....... a = 5
... elif e == ALTDIAGS:
....... a = 6
... elif e == REVDIAGS:
....... a = 7
... elif e == ALTREVDIAGS:
....... a = 8
... elif e == SPIRAL:
....... a = 9
... elif e == CCWSPIRAL:
....... a = 10
... return a

def test_1(e): # Enum by name
... a = 0
... if e == OnBy.ROWS:
....... a = 1
... elif e == OnBy.COLS:
....... a = 2
... elif e == OnBy.ALTROWS:
....... a = 3
... elif e == OnBy.ALTCOLS:
....... a = 4
... elif e == OnBy.DIAGS:
....... a = 5
... elif e == OnBy.ALTDIAGS:
....... a = 6
... elif e == OnBy.REVDIAGS:
....... a = 7
... elif e == OnBy.ALTREVDIAGS:
....... a = 8
... elif e == OnBy.SPIRAL:
........ a = 9
... elif e == OnBy.CCWSPIRAL:
....... a = 10
... return a

def test_2(e): # Enum by value
... a = 0
... if e == OnBy.ROWS.value:
....... a = 1
... elif e == OnBy.COLS.value:
....... a = 2
... elif e == OnBy.ALTROWS.value:
....... a = 3
... elif e == OnBy.ALTCOLS.value:
....... a = 4
... elif e == OnBy.DIAGS.value:
....... a = 5
... elif e == OnBy.ALTDIAGS.value:
....... a = 6
... elif e == OnBy.REVDIAGS.value:
....... a = 7
... elif e == OnBy.ALTREVDIAGS.value:
....... a = 8
... elif e == OnBy.SPIRAL.value:
....... a = 9
... elif e == OnBy.CCWSPIRAL.value:
....... a = 10
... return a

def test_3(e): # dict
... a = 0
... if e == 'ROWS':
....... a = 1
... elif e == 'COLS':
....... a = 2
... elif e == 'ALTROWS':
....... a = 3
... elif e == 'ALTCOLS':
....... a = 4
... elif e == 'DIAGS':
....... a = 5
... elif e == 'ALTDIAGS':
....... a = 6
... elif e == 'REVDIAGS':
....... a = 7
... elif e == 'ALTREVDIAGS':
....... a = 8
... elif e == 'SPIRAL':
....... a = 9
.... elif e == 'CCWSPIRAL':
....... a = 10
... return a

def get_onby():
... ret = {'ROWS': 0, 'COLS': 1, 'ALTROWS': 2, 'ALTCOLS': 3, \

...........'DIAGS': 4, 'ALTDIAGS': 5, 'REVDIAGS': 6, \
.......... 'ALTREVDIAGS': 7, 'SPIRAL': 8, 'CCWSPIRAL': 9}

... return ret

if __name__ == '__main__':
... main()

Test 0 time: Avg: 1.71 sec.
1.6797280311584473
1.7336883544921875
1.7154099941253662

Test 1 time: Avg. 7.70 sec.
7.6069886684417725
7.565075635910034
7.932457447052002

Test 2 time: Avg. 19.02 sec.
19.409813165664673
19.193800687789917
18.846235752105713

Test 3 time: Avg. 1.53 sec.
1.6147191524505615
1.5498807430267334
1.534912347793579

dict .......... 1.53 sec.
global ints ... 1.71 sec.
enum name ..... 7.70 sec.
enum value ... 19.02 sec.

This surprised me, since I was expecting enum to be optimized for speed on top of convenience, and because I wasn’t expecting dict objects to do well in handling strings as keys. But, there’s little difference between using a dict and just raw global ints, with dicts coming out slightly ahead.

So now I have to rewrite my route transposition code. However, the GUIs aren’t as work-intensive, and I’ll keep using enum.value for those.

Next up: No idea

Published by The Chief

Who wants to know?

Leave a comment

Design a site like this with WordPress.com
Get started