views:

162

answers:

3

OK, I'll admit upfront this is a mega kludge and that I could definately implement this better. It's only morbid curiosity that's driving me to find out how I could do this.

class SomeClass(object):
    def __init__(self):
        def __(self, arg):
            self.doStuff(arg)
        self.overLoaded = __
    def doStuff(self, string):
        print string

SomeClass().overLoaded("test string")

This returns a parameter error because I'm only supplying overLoaded() with one argument instead of two. Is there some magic to tell the interpreter that it's now a method of a class (I tried decorating it with @classmethod, I always understood this to be it's purpose??)

A: 

The issue is that you are trying to add a new instance method (not class method) and it is not binding properly. Python has a module function to manually bind functions to instances.

import new
self.method = new.instancemethod(func, self, class)

Edit: Apparently the new module is deprecated. Use the types module instead for metamagic.

import types
self.method = types.MethodType(func, self, class)
fengb
+4  A: 

Don't worry about the self parameter, the function already has that from local scope.

class SomeClass(object):
    def __init__(self):
        def __(arg):
            self.bar(arg)
        self.foo = __
    def foo(self, arg):
        print "foo", arg
    def bar(self, arg):
        print "bar", arg

SomeClass().foo("thing") # prints "bar thing"

When creating an instance (after __new__, iirc, but before __init__) Python binds all the methods to automagically supply the instance as the first argument. If you're adding a method later then you need to supply the instance manually. As you are defining the function with self already in scope you don't need to pass it again.

Python's new module is not a solution as it has been deprecated since 2.6. If you want to create a "real" instance method do it with the partial decorator like this:

import functools

class SomeClass(object):
    def __init__(self):
        def __(self, arg):
            self.bar(arg)
        self.foo = functools.partial(__, self)
    def foo(self, arg):
        print "foo", arg
    def bar(self, arg):
        print "bar", arg

SomeClass().foo("thing") # prints "bar thing"
sj26
That is some awesome magic right there. Thanks!
Richo
What if the function you want to plug is defined outside the class init? In that case, self is not into scope. I always forget how to handle this case, because you are basically assigning an unbound method to an instance, it does not make it bound automatically...
Stefano Borini
sj26 Covered that in his second stanza with the partial() call.
Richo
A: 

sj26's solution is a good one. Another alternative, if you want to set up a method that can be overloaded with any user-supplied function or with another of the object's methods, is build a custom descriptor. This descriptor can be used as a decorator (analogous to @classmethod or @staticmethod); and it allows you to store a function in an instance's dictionary, and returns it as a method:

import types

class overloadable(object):
    def __init__(self, func):
        self._default_func = func
        self._name = func.__name__

    def __get__(self, obj, type=None):
        func = obj.__dict__.get(self._name, self._default_func)
        return types.MethodType(func, obj, type)

    def __set__(self, obj, value):
        if hasattr(value, 'im_func'): value = value.im_func
        obj.__dict__[self._name] = value

    def __delete__(self, obj):
        del obj.__dict__[self._name]

Now we can just decorate a function with "@overloadable":

class SomeClass(object):
    def doStuff(self, string):
        print 'do stuff:', string
    @overloadable
    def overLoaded(self, arg):
        print 'default behavior:', arg

And it'll just do the right thing when we overload it for a given instance:

>>> sc = SomeClass()
>>> sc.overLoaded("test string")    # Before customization
default behavior: test string

>>> sc.overLoaded = sc.doStuff      # Customize
>>> sc.overLoaded("test string")
do stuff: test string

>>> del sc.overLoaded               # Revert to default behavior
>>> sc.overLoaded("test string")
default behavior: test string
Edward Loper