xZise
xZise

Reputation: 2379

Dynamically call overridden function

I iterate through a list and want to call a function on each item, but this function should be replaceable.

For example I have the following script:

class Parent(object):
    def a(self, text):
        raise NotImplementedError("called Parent.a")
    def b(self, text):
        raise NotImplementedError("called Parent.b")

class ChildA(Parent):
    def a(self, text):
        return "A.a: {}".format(text)
    def b(self, text):
        return "A.b: {}".format(text)

class ChildB(Parent):
    def a(self, text):
        return "B.a: {}".format(text)
    def b(self, text):
        return "B.b: {}".format(text)

# the separation is ONLY so that the first exec_all doesn't fail
# in my production code it's a list of mixed instances
children = [ 
    ChildA(), # obviously here might be several different ChildA instances
]
childrenMixed = children + [
    ChildB(), # obviously here might be several different ChildB instances
]

def exec_all(method, children):
    for child in children:
        try:
            print(method(child, "Hello world"))
        except Exception as e:
            print("Unable to call method for child '{}': {}".format(child, e.message))

exec_all(ChildA.a, children) # works
exec_all(ChildA.b, children) # works
exec_all(ChildA.a, childrenMixed) # TypeError
exec_all(ChildA.b, childrenMixed) # TypeError
exec_all(Parent.a, childrenMixed) # NotImplementError
exec_all(Parent.b, childrenMixed) # NotImplementError

The first two exec_all does work fine, but the next two don't work, because it tries to call ChildA.a which doesn't exists in ChildB. And the last two raise the NotImplementedError.

It should look something like this:

A.a: Hello world # 1st exec_all
A.b: Hello world # 2nd exec_all
A.a: Hello world # 3rd exec_all
B.a: Hello world # but TypeError
A.b: Hello world # 4th exec_all 
B.b: Hello world # but TypeError
A.a: Hello world # 5th exec_all
B.a: Hello world # but NotImplementError
A.b: Hello world # 6th exec_all 
B.b: Hello world # but NotImplementError

So how do I support multiple subclasses of Parent?

Upvotes: 0

Views: 122

Answers (2)

unutbu
unutbu

Reputation: 879361

Pass the method name, not the method. Use getattr(child, methodname) to obtain the method:

class Parent(object):
    def a(self, text):
        raise NotImplementedError("called Parent.a")
    def b(self, text):
        raise NotImplementedError("called Parent.b")

class ChildA(Parent):
    def a(self, text):
        return "A.a: {}".format(text)
    def b(self, text):
        return "A.b: {}".format(text)

class ChildB(Parent):
    def a(self, text):
        return "B.a: {}".format(text)
    def b(self, text):
        return "B.b: {}".format(text)

children = [ ChildA(), ]
childrenMixed = children + [ ChildB(), ]

def exec_all(methodname, children):
    for child in children:
        method = getattr(child, methodname)
        print(method("Hello world"))
    print

exec_all('a', children) 
exec_all('b', children) 
exec_all('a', childrenMixed) 
exec_all('b', childrenMixed) 
exec_all('a', childrenMixed) 
exec_all('b', childrenMixed) 

yields

A.a: Hello world

A.b: Hello world

A.a: Hello world
B.a: Hello world

A.b: Hello world
B.b: Hello world

A.a: Hello world
B.a: Hello world

A.b: Hello world
B.b: Hello world

In Python2, ChildA.a is an unbound method. Unlike Python3, unbound methods check if the first argument is an instance of the correct class -- in this case ChildA. This is why calling

ChildA.a(ChildB(), text)

raises the TypeError:

TypeError: unbound method a() must be called with ChildA instance as first argument (got ChildB instance instead)

In Python3, such a call would be okay, though if you are doing this it would probably be better to make all these methods plain functions rather than methods.


It sounds like you really want to preserve the form of your function calls as you posted them:

exec_all(ChildA.a, children) 
exec_all(ChildA.b, children) 
exec_all(ChildA.a, childrenMixed) 
exec_all(ChildA.b, childrenMixed) 
exec_all(Parent.a, childrenMixed) 
exec_all(Parent.b, childrenMixed) 

If we take this as a fixed requirement, then you could get the desired behavior by defining exec_all as follows:

class Parent(object):
    def a(self, text):
        raise NotImplementedError("called Parent.a")
    def b(self, text):
        raise NotImplementedError("called Parent.b")

class ChildA(Parent):
    def a(self, text):
        return "A.a: {}".format(text)
    def b(self, text):
        return "A.b: {}".format(text)

class ChildB(Parent):
    def a(self, text):
        return "B.a: {}".format(text)
    def b(self, text):
        return "B.b: {}".format(text)

children = [ 
    ChildA(), 
]
childrenMixed = children + [
    ChildB(), 
]

def exec_all(method, children):
    methodname = method.__name__
    for child in children:
        method = getattr(child, methodname)
        print(method("Hello world"))

exec_all(ChildA.a, children) 
exec_all(ChildA.b, children) 
exec_all(ChildA.a, childrenMixed) 
exec_all(ChildA.b, childrenMixed) 
exec_all(Parent.a, childrenMixed) 
exec_all(Parent.b, childrenMixed) 

But passing the wrong method on purpose is not a good design. You shouldn't pass ChildA.a when you want ChildB.a to be called. Python does not make this simple because this is not the way OOP is intended to work. Either passing the correct methods or passing method names as strings (as shown above) are better options.

Upvotes: 0

Lev Levitsky
Lev Levitsky

Reputation: 65791

Are you after something like this?

exec_all(lambda x: x.a())

or:

def call_a(obj):
    return obj.a()
exec_all(call_a)

Upvotes: 2

Related Questions