开发者

Writing a class that accepts a callback in Python?

I need to write a class that allo开发者_如何学Cws 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!


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: ")


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.


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: ")


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...")


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!


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜