OO approach to decision sequence?

C

Chinook

OO approach to decision sequence?
---------------------------------

In a recent thread (Cause for using objects?), Chris Smith replied with (in
part):
If your table of photo data has several types of photos, and you find
yourself saying

if is_mugshot:
#something
elif is_freehand:
#something
else:
#something

then OOP will help organize your code.

This struck a chord because I'm trying to refactor a top-down approach to an
OO approach. The reason I am doing such is to try and get my mind wrapped
around OO design, not because the particular module will benefit from an OO
approach (it's a simple top-down recursive tree utility). In fact it's
probably ill suited for OO and that is why I chose it.

I've used an OO approach where I built up record (instance) content in a
variable record file, but here I'm trying to come at it from the opposite
direction as variable record mapping (deconstructing) would be to such.

Anyway, a tree node can be any of seven types where:

if node_type_1:
# recurse
elif node_type_2:
# terminus - do something
elif node_type_3:
# terminus - do something else
...
else:
# terminus - catch all, do yet something else
return #to parent

So, where is the magic :~) Seriously, how might OO help me organize this
type of problem (alleviate the conventional lengthy if structure)? I've
looked at the cookbook, class interface techniques, factory functions and
metaclasses till my head is swimming. Am I missing a bolt in the machinery
somewhere, or simply trying to find magic that doesn't exist for a
straightforward decision sequence?

Thank you,
Lee C
 
J

Jordan Rastrick

I've coded some simple recursive tree data structures using OO before
(unfortunately not in Python though). It's not nessecarily an
ill-suited approach to the task, although it depends on the specific
details of what you're doing. What's the the piece of code from which
your if...elif fragment is taken actually supposed to do?

Without knowing more about your problem, I think the most obvious OO
approach would be to write a seperate (simple) class for each of
node_type_1, node_type_2, etc. Make sure each one provides the same
interface, i.e. defines the same set of methods. Then put the different
decision branches as implementations of a certain method in each class.

class Node_Type_1(object):
def recurse(self):
# do stuff here

class Node_Type_2(object):
def recurse(self):
# do other stuff here

only make sure you use more informative names than "Node_Type_1" and
"recurse" :)

Your if elif code then collapses to:

node.recurse()

where the node variables refers to an instance of any one of your
Node_Type classes.

OO isn't magic. You still in the end have to write the code that
implements the decision choices. In this example, its likely the OO
code is actually more verbose, since you have all the class and method
definitions to write as well.

But there are advantages. Adding new cases (new node_types) is simpler
and less error prone - you just write a new node_type class. The code
for the new class, unlike a new elif brach, is kept cleanly and safely
seperate from your existing, correctly working code. If you forget to
provide the new class with the recurse method, you get a runtime error.
But if you forget to add the case to your if..elif statement, you just
end up silently going with your sentinel "else" branch, which may not
be what you really want to do with that node_type. (in fact I'd usually
raise an exception on reaching the else branch in any such extended
if..elif structure, because it's almost always the result of an error
in your program logic)

Essentially, although you may end up writing more code, its also better
organised, since as you wish it avoids your 'conventional lengthy if
struture'.

Its a partciularily useful approach if you have several different
functions that need to branch on node-type in this fashion (e.g. pretty
print the tree, post-order traverse it, run a search over it, etc). For
each such function, you just provide a method on every node_type class.
Again, if you forget to provide the code for a certain node_type, the
error is very easy to detect.

If your problem is simple, the OO approach may be overkill - your
current if..elif code may be the most straightfoward solution. But if
the problem is complicated, or has the potential to grow more
complicated, the advantages of the OO approach may be a worthwhile
trade off. Fortuantely Python is a multi-paradigm language, so the
choice to use or not use OO according to what best suits the design and
the problem is left up to you, the programmer, and is not thrust upon
you by the language.

And you certainly don't want metaclasses or anything else that complex
and deep for something like this.
 
J

John Machin

Jordan said:
I've coded some simple recursive tree data structures using OO before
(unfortunately not in Python though). It's not nessecarily an
ill-suited approach to the task, although it depends on the specific
details of what you're doing. What's the the piece of code from which
your if...elif fragment is taken actually supposed to do?

Without knowing more about your problem, I think the most obvious OO
approach would be to write a seperate (simple) class for each of
node_type_1, node_type_2, etc. Make sure each one provides the same
interface, i.e. defines the same set of methods. Then put the different
decision branches as implementations of a certain method in each class.

class Node_Type_1(object):
def recurse(self):
# do stuff here

etc etc

and perhaps if you found by the time you got to Node_Type_2 that its
opensesame() method was identical to the opensesame() method for
Node_Type_1, you might decide that having separate simple classes could
be improved on by having a Node class, which you would subclass for each
Node_Type_n ...
 
T

Thomas Lotze

Jordan said:
Without knowing more about your problem, I think the most obvious OO
approach would be to write a seperate (simple) class for each of
node_type_1, node_type_2, etc.

While I agree that this is the cleanest and usually simplest approach,
it does have its drawbacks. I'm currently working on a project where I'd
very much like to avoid writing a whole set of classes just for the
purpose of avoiding a decision chain.

For a PDF library, I need basic data types that are used in a PDF
document. Such are integers, floats, strings, lists, dictionaries and a
few. At some point they have to be written to a file, and at first I was
tempted to create types like pdfint, pdffloat, pdfstr etc. which
implement the respective file encoding either in a write method or
directly in __str__.

However, the whole point of the library is to allow working with the
document's data. Beside manipulating existing (as in read from a PDF
file) mutable objects this includes creating new objects of type pdffoo.
And I realized it is very bothersome to have to say x = pdfint(5)
instead of x = 5 everytime I deal with integers that would end up in the
document. Similar for, e.g., adding to PDF integers: x = pdfint(y+z)
instead of just x = y+z.

The latter can be cured by touching all methods returning any pdffoo
instances. No sane person would do this, however, and it would not
eliminate any pdffoo(x) type conversions in the app code anyway.

So I decided that in this case it is best to go without special types
and use those provided by Python, and live with an ugly decision chain
or two at defined places in the library.
 
P

Paul McGuire

Lee C -

Here is a technique for avoiding the if-elseif-elseif...-else method
for building objects. It is a modified form of ChainOfResponsibility
pattern, in which you have a collection of factory methods that all
have a common signature, or a collection of Factory classes that all
implement a "makeObject" method. These common methods probably take a
string, and return a generic object. Fortunately, such type
flexibility is *very* easy in Python. :)

Example:
I want to convert individual strings to native data types. I want to
detect integers, reals, complex numbers, and booleans (indicated by
'True' or 'False'). This is kind of similar to a parsing problem, but
it could also be used for deserializing or unpickling data of an
unknown original type.

Note the special treatment I have to go through with boolean values - I
needed to write a special makeBool routine, since Python will take any
non-empty string to be True, when what I want is 'True' to yield true
and 'False' to yield false.

Hope this gives you some alternative ideas to your cascading if's.

-- Paul


def makeBool(s):
if s in ('True','False'):
return s == 'True'
raise ValueError

converters = [ int, float, complex, makeBool, str ]

def makeObject(stringVar):
for conv in converters:
try:
val = conv(stringVar)
except Exception:
continue
else:
break;
return val

def test(s):
val = makeObject(s)
print s, val, type(val)

test('1')
test('1.0')
test('1+2j')
test('1+0j')
test('True')
test('False')
test('A')

prints:
1 1 <type 'int'>
1.0 1.0 <type 'float'>
1+2j (1+2j) <type 'complex'>
1+0j (1+0j) <type 'complex'>
True True <type 'bool'>
False False <type 'bool'>
A A <type 'str'>
 
G

George Sakkis

Paul McGuire said:
Lee C -

Here is a technique for avoiding the if-elseif-elseif...-else method
for building objects. It is a modified form of ChainOfResponsibility
pattern, in which you have a collection of factory methods that all
have a common signature, or a collection of Factory classes that all
implement a "makeObject" method. These common methods probably take a
string, and return a generic object. Fortunately, such type
flexibility is *very* easy in Python. :)

Example:
I want to convert individual strings to native data types. I want to
detect integers, reals, complex numbers, and booleans (indicated by
'True' or 'False'). This is kind of similar to a parsing problem, but
it could also be used for deserializing or unpickling data of an
unknown original type.

Note the special treatment I have to go through with boolean values - I
needed to write a special makeBool routine, since Python will take any
non-empty string to be True, when what I want is 'True' to yield true
and 'False' to yield false.

Hope this gives you some alternative ideas to your cascading if's.

-- Paul


def makeBool(s):
if s in ('True','False'):
return s == 'True'
raise ValueError

converters = [ int, float, complex, makeBool, str ]

def makeObject(stringVar):
for conv in converters:
try:
val = conv(stringVar)
except Exception:
continue
else:
break;
return val

def test(s):
val = makeObject(s)
print s, val, type(val)

test('1')
test('1.0')
test('1+2j')
test('1+0j')
test('True')
test('False')
test('A')

prints:
1 1 <type 'int'>
1.0 1.0 <type 'float'>
1+2j (1+2j) <type 'complex'>
1+0j (1+0j) <type 'complex'>
True True <type 'bool'>
False False <type 'bool'>
A A <type 'str'>

Nice technique. Something that needs to be pointed out is that the
order of the converters *is* important; int takes precedence over
float, which take precedence over complex and bool takes precedence
over string. More succinctly:
{ int -> float -> complex }
{ bool -> str }
In general the converters will form a strict partially ordered set, so
the list of converters should be a topological sort of the respective
DAG.

George
 
C

Chinook

Paul McGuire said:
Lee C -

Here is a technique for avoiding the if-elseif-elseif...-else method
for building objects. It is a modified form of ChainOfResponsibility
pattern, in which you have a collection of factory methods that all
have a common signature, or a collection of Factory classes that all
implement a "makeObject" method. These common methods probably take a
string, and return a generic object. Fortunately, such type
flexibility is *very* easy in Python. :)

Example:
I want to convert individual strings to native data types. I want to
detect integers, reals, complex numbers, and booleans (indicated by
'True' or 'False'). This is kind of similar to a parsing problem, but
it could also be used for deserializing or unpickling data of an
unknown original type.

Note the special treatment I have to go through with boolean values - I
needed to write a special makeBool routine, since Python will take any
non-empty string to be True, when what I want is 'True' to yield true
and 'False' to yield false.

Hope this gives you some alternative ideas to your cascading if's.

-- Paul


def makeBool(s):
if s in ('True','False'):
return s == 'True'
raise ValueError

converters = [ int, float, complex, makeBool, str ]

def makeObject(stringVar):
for conv in converters:
try:
val = conv(stringVar)
except Exception:
continue
else:
break;
return val

def test(s):
val = makeObject(s)
print s, val, type(val)

test('1')
test('1.0')
test('1+2j')
test('1+0j')
test('True')
test('False')
test('A')

prints:
1 1 <type 'int'>
1.0 1.0 <type 'float'>
1+2j (1+2j) <type 'complex'>
1+0j (1+0j) <type 'complex'>
True True <type 'bool'>
False False <type 'bool'>
A A <type 'str'>

Nice technique. Something that needs to be pointed out is that the
order of the converters *is* important; int takes precedence over
float, which take precedence over complex and bool takes precedence
over string. More succinctly:
{ int -> float -> complex }
{ bool -> str }
In general the converters will form a strict partially ordered set, so
the list of converters should be a topological sort of the respective
DAG.

George

Ah yes, there is more than one way to skin a cat :~) and your samples are
helping me get a better grasp of both Python and OOP in such. I find both
Bengt's and Paul's variation on a theme understandable (I must be making
progress :~) and interesting. I must admit that I did have to look twice at
Bengt's clever little slice on 'an' though.

I had already worked my way through <a
href="http://fraca7.free.fr/blog/index.php?2005/02/28/2-design-patterns-part-
i---chain-of-responsibility">this</a> though, so they were that much more
understandable.

Actually, I'm using a superclass as a factory, similar to Steve B's example
(as modified by someone I can't find the name of). The difference is that my
criteria does not map easily to class names so I have the verbose decision
sequence in my superclass. Of course, if I want to get really fancy, I
could use the COR pattern within such.

I'm making more hay out of this one little pasture than I expected (i.e.
increasing my understanding) thanks to all of you. I'm going to have to
start writing down names (rather than rely on this ol head) so I can properly
thank everyone - like those clarifying other examples.

At some point I'll put up the original top-down and OO refactored versions of
my little utility in the hope that I will learn even more from criticism of
my efforts. The "new deal" top-down structured programming encountered in my
continuing post grad work at BU in the 1960s was easier to get a solid handle
on ;')

Much appreciated,

Lee C

"Wonder rather than doubt is the root of knowledge." --Abraham Joshua Heschel
 
P

Paul McGuire

Ok, I'm glad you guys liked that design pattern. Here are a few
additional footnotes:

1. As George mentions, the order of the converters is *very* important,
especially in this particular case. One might question whether '1+0j'
would convert to a complex or an int - my first thought was to make it
an int, but that made the example more complicated, and I didn't really
need to be fussy to make the example.

2. If this were a design for a deserializer, it would be a bit poor, if
only because of this order-sensitivity. Also, there is no way to
reconstruct a string containing a value that maps to one of the other
types, such as 'True' or '1'. A better deserialize/serialize design
would support quoted strings so that a string of '"1"' would not be
misinterpreted as an int. But again, I didn't want to take on
deconstructing quoted strings, escaped characters, etc. But in
general, when designing a deserializer/serializer, it's better to make
the output more unambiguous.

3. I once had an O-O expert tell me that this was not really a
Chain-of-Responsibility pattern. His take was that, in COR, each link
succeeds, or calls the next link in the chain. In my design, each link
is just one of a collection, and succeeds or raises an exception; the
iteration is done from the calling routine. I really haven't found a
name for this pattern, so I just call it "modified COR".

-- Paul
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,763
Messages
2,569,562
Members
45,038
Latest member
OrderProperKetocapsules

Latest Threads

Top