Pergunta

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

Foi útil?

Solução

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!!!
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top