10

I need to write a class that allows a subclass to set an attribute with the name of a function. That function must then be callable from instances of the class.

For example, I say I need to write a Fruit class where the subclass can pass in a welcome message. The Fruit class must expose an attribute print_callback that can be set.

class Fruit(object):
    print_callback = None

    def __init__(self, *args, **kwargs):
        super(Fruit, self).__init__(*args, **kwargs)
        self.print_callback("Message from Fruit: ")

I need to expose an API that is can be consumed by this code (to be clear, this code cannot change, say it is 3rd party code):

def apple_print(f):
    print "%sI am an Apple!" % f

class Apple(Fruit):
    print_callback = apple_print

If I run:

mac = Apple()

I want to get:

Message from Fruit: I am an Apple!

Instead I get:

TypeError: apple_print() takes exactly 1 argument (2 given)

I think this is because self is passed in as the first argument.

So how do I write the Fruit class? Thanks!

2
  • 1
    I appreciate the problems you've been having with this approach and the excellent solutions below. However this looks to me like you should be defining print_callback as a method (maybe just called print_me, and getting subclasses to override it. That would be simple and pythonic. For every different kind of print_me method, make a different subclass. Commented Oct 14, 2009 at 20:13
  • My motivation for posing the question was that I wanted to subclass Google App Engine's google.appengine.ext.db.djangoforms (e.g. as DF2) to allow override of an attribute formfield_callback in the same manner as Django's ModelForm (django.forms.ModelForm). The idea is that 'class f(DF2)' could then be used by standard Django simply by changing the superclass to django.forms.ModelForm. I don't know why the Django authors defined formfield_callback as an attribute. Commented Oct 22, 2009 at 2:50

7 Answers 7

11

Python assumes that any functions bound within a class scope are methods. If you'd like to treat them as functions, you have to dig around in their attributes to retrieve the original function object:

def __init__(self, *args, **kwargs):
    super(Fruit, self).__init__(*args, **kwargs)

    # The attribute name was changed in Python 3; pick whichever line matches
    # your Python version.
    callback = self.print_callback.im_func  # Python 2
    callback = self.print_callback.__func__ # Python 3

    callback("Message from Fruit: ")
Sign up to request clarification or add additional context in comments.

3 Comments

Or better yet, just add a self argument to the function so it can act like a normal method.
According to the question, the callback definition cannot be changed.
BTW, also worth noting is that the im_func attribute of methods got renamed to __func__ in Python 3.x.
6

You can use directly:

class Apple(Fruit):
    print_callback = staticmethod(apple_print)

or:

class Apple(Fruit):
    print_callback = classmethod(apple_print)

In the first case, you'll get only one parameter (the original). In the second, you'll receive two parameters where the first will be the class on which it was called.

Hope this helps, and is shorter and less complex.

1 Comment

"to be clear, this code cannot change, say it is 3rd party code"
3

Updated: incorporating abourget's suggestion to use staticmethod:

Try this:

def __init__(self, *args, **kwargs):
    super(Fruit, self).__init__(*args, **kwargs)

    # Wrap function back into a proper static method
    self.print_callback = staticmethod(self.print_callback)

    # And now you can do:
    self.print_callback("Message from Fruit: ")

Comments

3

I was looking for something more like this when I found this question:

class Something:
    def my_callback(self, arg_a):
        print arg_a

class SomethingElse:
    def __init__(self, callback):
        self.callback = callback

something = Something()
something_else = SomethingElse(something.my_callback)
something_else.callback("It works...")

Comments

1

There's also a bit dirtyer solution with metaclasses:

def apple_print(f):
    print "Apple " + f

class FruitMeta(type):
    def __new__(cls, name, bases, dct):
        func = dct["print_callback"]
        dct["print_callback"]=lambda x,f,func=func: func(f)
        return type.__new__(cls,name,bases,dct)

class Fruit(object):
    __metaclass__ = FruitMeta
    print_callback = None
    def __init__(self):
        super(Fruit,self).__init__()
        self.print_callback("Msg ")

class Apple(Fruit):
    print_callback = apple_print

mac = Apple()here

It manipulates the class before its creation!

Comments

0

You can use Python 3's __func__ or Python 2 im_func to get function object and call it directly. For global functions it will work ok. But if you want to call function for specific class instance, you need to provide class instance as first argument to callback function.

Here is example:

class Test1:
    def __init__(self):
        self.name = 'Test1 instance'

    def OnPrint(self, value):
        assert isinstance(self, Test1), 'Wrong self argument type'
        print(f'{self.name}: Test1.OnPrint with value {value}')

t1 = Test1()
f1 = t1.OnPrint.__func__

print('1')
f1(t1, 5)

# Will print:
#     Test1 instance: Test1.OnPrint with value 5
print('2')

#f1(1,2)
#AssertionError: Wrong self argument type

#f1(2)
#TypeError: Test1.OnPrint() missing 1 required positional argument: 'value'

Please note that you can omit assert isinstance if you're confident that call is right one.

Comments

0

I encountered this problem recently (and couldn't believe I hadn't seen it before, but anyway). If storing the callback function as a property doesn't work ( because as John Millikin points out, Python assumes a function bound within class scope is a method (and so passes self to it)), then you can get round it by storing the callback in a container (e.g. a list or tuple) and then Python won't assume it's a method...

def foo(x):
    print("This is foo", x)

def bar(x):
    print("This is bar",x)

class A():
    callback = [foo]

    def doit(self,x):
        self.callback[0](x)

class B(A):
    callback = [bar]

a = A()
a.doit(42)
# This is foo 42

b = B()
b.doit(42)
# This is bar 42

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.