4

I am interested in calling an instance method as both a class method as well as an instance method. This can be done by using the class_or_instance decorator as follows:

class class_or_instance(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, obj, cls):
        if obj is not None:
            return lambda *args, **kwds: self.fn(obj, *args, **kwds)
        else:
            return lambda *args, **kwds: self.fn(cls, *args, **kwds)

class A(object):
    @class_or_instance
    def func1(self,*args):
         # method body

Now I can call func1 either as A.func1(*args) or A().func1(*args). However when doing this the docstring of func1 disappears. One way to deal with this would be to use decorator from decorator.py but I am having trouble getting this to work with a decorator that is a class rather than a function. Any suggestions on how to go about this?

EDIT : functools.wraps() won't work correctly in this case. See related question on stackoverflow

Community
  • 1
  • 1
rk7
  • 65
  • 8
  • Why not use @classmethod or @staticmethod? – Ulrich Eckhardt Jun 18 '13 at 04:03
  • @classmethod will only allow it to behave as a class method. I want to be able to call it either as a class or as an instance method – rk7 Jun 18 '13 at 05:00
  • If you call a classmethod on an instance, the first parameter is not a reference to the instance ("self") but to its class ("cls"). The only difference to a staticmethod is that this first "cls" parameter is missing, but otherwise they behave the same. – Ulrich Eckhardt Jun 18 '13 at 19:33

1 Answers1

3

Basic Descriptor/Decorator

You just need to keep in mind which function you should decorate. Your function is being created in __get__, so it won't help to use the wrapper as a decorator, instead, you need to apply it in the __get__ method. As an aside, you can use either functools.update_wrapper or decorators.decorator for this. They work very similarly, except that you have to keep the result of decorators.decorator whereas functools.update_wrapper returns None. Both have signature f(wrapper, wrapped).

from functools import update_wrapper
class class_or_instance(object):
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, obj, cls):
        if obj is not None:
            f = lambda *args, **kwds: self.fn(obj, *args, **kwds)
        else:
            f = lambda *args, **kwds: self.fn(cls, *args, **kwds)
        # update the function to have the correct metadata
        update_wrapper(f, self.fn)
        return f

class A(object):
    @class_or_instance
    def func1(self,*args):
        """some docstring"""
        pass

Now if you do:

print A.func1.__doc__

You'll see "some docstring". Yay!


Cached property decorator

The key here is that you can only affect what gets returned. Since class_or_instance doesn't actually serve as the function, it doesn't really matter what you do with it. Keep in mind that this method causes the function to be rebound every time. I suggest you add a little bit of magic instead and bind/cache the function after the first call, which really just involves adding a setattr call.

from functools import update_wrapper
import types

class class_or_instance(object):
    # having optional func in case is passed something that doesn't have a correct __name__
    # (like a lambda function)
    def __init__(self, name_or_func):
        self.fn = fn
        self.name = fn.__name__

    def __get__(self, obj, cls):
        print "GET!!!"
        if obj is not None:
            f = lambda *args, **kwds: self.fn(obj, *args, **kwds)
            update_wrapper(f, self.fn)
            setattr(obj, self.name, types.MethodType(f, obj, obj.__class__))
        else:
            f = lambda *args, **kwds: self.fn(cls, *args, **kwds)
            update_wrapper(f, self.fn)
        return f

And then we can test it out...neato:

A.func1 #GET!!!
obj = A()
obj.func1 #GET!!!
obj.func1 is obj.func1 # True
A.func1 # GET!!!
obj2 = A()
obj2.func1 is not obj.fun1 # True + GET!!!
Jeff Tratner
  • 16,270
  • 4
  • 47
  • 67
  • Thanks both the techniques you suggested do correctly return the docstrings. However using `decorator.decorator` has an issue with the number of arguments passed to the decorated method, and when the method is called it complains of `TypeError`. On the other hand the `update_wrapper` technique works without a hitch. I am wondering what's the issue with the first technique? – rk7 Jun 18 '13 at 13:19
  • @rk7 so `decorators.decorator` and `functools.update_wrapper` are *not* setup to be compatible with each other. I don't think you can use `decorators.decorator` because it's not set up to handle methods (that have self as the first argument)...it'd take a lot of hacking around to do it...not sure it's worth it. – Jeff Tratner Jun 20 '13 at 03:21