Chris
Chris

Reputation: 10101

How to add functions to a class based on a parameter

I've got a class (node) to which I want to assign a specific set of functions (in this case a1, a2, b1, and b2) based on a parameter of the class (operatingMode).

The situation is that I have a motor that has many different operating modes. Each operating mode allows for certain functions to be performed, but not others. The assignment of functions to various modes is done in a way that does not lend itself nicely to creating classes for each operating mode.

Below is my pass at a solution, but it doesn't work.

Any thoughts?

def a1(self):
    return 'a1'

def a2(self):
    return 'a2'

def b1(self):
    return 'b1'

def b2(self):
    return b2




class node(object):
    def __init__(self,operatingMode):
    self.operatingMode=operatingMode

    if self.operatingMode=='A':
        self.a1function=a1
        self.a2function=a2
        print 'Operating Mode \'A\' functions loaded'

    if self.operatingMode=='B':
        self.b1function=b1
        self.b2function=b2
        print 'Operating Mode \'B\' functions loaded'

    def setOperatingMode(self,operatingMode):
        self.operatingMode=operatingMode
        self.__init__(self,operatingMode)

Running this in my terminal lets me call it, but I have to state elbow twice:

In [65]: elbow=node('A')
Operating Mode 'A' functions loaded

In [66]: elbow.a1function(elbow)
Out[66]: 'a1'

trying to run elbow.setOperatingMode('B') yields an error.

Upvotes: 0

Views: 263

Answers (4)

unutbu
unutbu

Reputation: 880797

In response to this comment:

This is for motor control, so I'm trying to limit overhead... operating modes will be changed much more infrequently than individual commands will be called.

Python instances can change their class. You could use this to change operating modes without needing to use an if-clause to check the mode:

class Base(object):
    # Put any methods shared by ANode and BNode here.
    pass

class ANode(Base):
    def a1(self):
        return 'a1'

    def a2(self):
        return 'a2'

class BNode(Base):
    def b1(self):
        return 'b1'

    def b2(self):
        return 'b2'


elbow = ANode()
print(elbow.a1())
# a1

knee = ANode()
print(knee.a1())
# a1

elbow.__class__ = BNode
print(knee.a1())
# a1
print(elbow.b2())
# b2

elbow.a1()
# AttributeError: 'BNode' object has no attribute 'a1'

On the positive side, this is the fastest suggestion I've posted. Notice there are no if-statements in the code above. Once the class changes, all the available methods change along with it "instantly", purely due to normal Python method calling semantics.

If Node is defined as in the decorator solution,

In [33]: elbow = Node('A')

In [34]: %timeit elbow.a1()
1000000 loops, best of 3: 288 ns per loop

While, if knee is defined using ANode,

In [36]: knee = ANode()

In [37]: %timeit knee.a1()
10000000 loops, best of 3: 126 ns per loop

So this solution is more than 2x as fast at calling methods than the decorator solution.

Switching speed is comparable:

In [38]: %timeit elbow.operatingMode = 'B'
10000000 loops, best of 3: 71.7 ns per loop

In [39]: %timeit knee.__class__ = BNode
10000000 loops, best of 3: 78.7 ns per loop

Caveat: One thing that is going to plague all the solutions I've posted is that after a switch the names the the available methods change. That means when you program using these classes, you have to keep track of the state of the instance before you can even know what methods are available. That is awkward.

Your program will be much much simpler if ANode and BNode have exactly the same interface -- all the same attribute and method names -- with just the definitions of those methods to changing (when the mode -- or class -- is changed).


Regarding this comment:

I've got about a hundred functions and 7 operation modes. Of those functions, about 10 are shared between all operation modes, 75 are shared by multiple modes, and 15 are exclusive to a particular mode. The problem is that the 75 aren't allocated to the modes nicely: Some might be in modes 1,4 and 7, others in 2,4,5, and 7, others in 1 and 5.

You can define the methods outside the classes, and then "hook them up" to Mode-based classes like this:

def a1(self):
    return 'a1'

def a2(self):
    return 'a2'

def b1(self):
    return 'b1'

def b2(self):
    return 'b2'

class Base(object):
    # Put the 10 methods share by all modes here
    def common1(self): 
        pass

class ANode(Base):
    a1 = a1
    a2 = a2

class BNode(Base):
    b1 = b1
    b2 = b2

class CNode(Base):
    a1 = a1
    b2 = b2

Upvotes: 1

unutbu
unutbu

Reputation: 880797

Perhaps using a checkMode decorator would be a cleaner way -- this avoids __getattr__ and type.MethodType magic:

def checkMode(mode):
    def deco(func):
        def wrapper(self):
            if self.operatingMode == mode:
                return func(self)
            else:
                raise TypeError('Wrong operating Mode')
        return wrapper
    return deco

class Node(object):
    def __init__(self, operatingMode):
        self.operatingMode = operatingMode

    @checkMode('A')
    def a1(self):
        return 'a1'

    @checkMode('A')
    def a2(self):
        return 'a2'

    @checkMode('B')
    def b1(self):
        return 'b1'

    @checkMode('B')
    def b2(self):
        return 'b2'

with the code above, we can do this:

elbow = Node('A')
print(elbow.a1())
# a1

knee = Node('A')
print(knee.a1())
# a1

elbow.operatingMode = 'B'
print(knee.a1())  # knee is still in operatingMode A
# a1

print(elbow.b2())
# b2

elbow.a1()
# TypeError: Wrong operating Mode

Explanation:

The decorator syntax works as follows:

@deco
def func(): ...

is equivalent to

def func(): ...
func = deco(func)

Above, checkMode is a function which returns a decorator, deco. deco then decorates the methods a1, a2, etc., so that

a1 = deco(a1)

Thus, a1 is the func passed to deco. deco(a1), in turn, returns a new method, generically called wrapper. This new method gets assigned to a1 by the statement a1 = deco(a1). So a1 is now the method wrapper. So when you call elbow.a1(), the code in wrapper gets executed.

Upvotes: 1

unutbu
unutbu

Reputation: 880797

Using types.MethodType and __getattr__:

import types

def a1(self):
    return 'a1'

def a2(self):
    return 'a2'

def b1(self):
    return 'b1'

def b2(self):
    return 'b2'

class Node(object):
    def __init__(self, operatingMode):
        self.operatingMode = operatingMode
    def __getattr__(self, attr):
        if self.operatingMode=='A':
            if attr == 'a1function':
                return types.MethodType(a1, self)
            if attr == 'a2function':
                return types.MethodType(a2, self)

        elif self.operatingMode=='B':
            if attr == 'b1function':
                return types.MethodType(b1, self)
            if attr == 'b2function':
                return types.MethodType(b2, self)
        else:
            raise AttributeError()

Then

elbow = Node('A')
print(elbow.a1function())
elbow.operatingMode = 'B'
print(elbow.b2function())

yields

a1
b2

Upvotes: 2

abarnert
abarnert

Reputation: 366113

Your entire design is very bizarre, and you probably need to take a step back and explain what you're trying to do so someone can help you with a better design.

But if you just want to make your design work, there are two problems with your current code:

self.a1function = a1

This sets self.a1function to a regular function, not a bound method. You can explicitly create a bound method like this:

self.a1function=types.MethodType(a1, self, self.__class__)

Or you can set a1function to be a wrapper around a1. Or, more simply, do the wrapping dynamically, which means you can fake the bound-method-ness with a closure, which is arguably more readable:

def __getattr__(self, attr):
    if attr == 'a1function' and self.operating_mode == 'A':
        return lambda: a1(self)

Meanwhile:

trying to run elbow.setOperatingMode('B') yields an error.

You really need to post the traceback, or at least the error string, instead of just saying "yields an error". In this case, it tells you very explicitly what the error is, so you don't need to guess:

TypeError: __init__() takes exactly 2 arguments (3 given)

The problem is that in this line:

self.__init__(self,operatingMode)

… you're passing self twice. It's the object you call the method on, and it's also the first parameter.

Calling __init__ from another method is a bad idea anyway, but if you really want to, it's the same as calling any other method:

self.__init__(operatingMode)

Upvotes: 0

Related Questions