简体   繁体   中英

decorating a class function with a callable instance

I'm trying to decorate a function by replacing it with a instance of a callable class:

class FunctionFaker( object ):
    def __init__( self, f ):
        self.f= f
    def empty_function( self ):
        pass
    def __call__( self, *args, **kwargs ):
        self.f( *args, **kwargs)

def fakefunction( f ):
    '''a decorator that transforms a function into a FunctionFaker'''
    return FunctionFaker(f)

@fakefunction
def dosomething():
    pass

dosomething.empty_function()
dosomething()

This works as expected.

However, as soon as I try to decorate a class method:

class Test( object ):
    @fakefunction
    def dosomething(self):
        pass

t=Test()
t.dosomething.empty_function()
t.dosomething()

I get a TypeError: dosomething() takes exactly 1 argument (0 given) .

Now, I think I can answer the why :

To support method calls, functions include the __get__() method for binding methods during attribute access. This means that all functions are non-data descriptors which return bound or unbound methods depending whether they are invoked from an object or a class.

So, FunctionFaker, not being a function, doesn't have the said descriptor, thus not mangling the arguments.

How can I implement a callable class that is able to replace a instance method?

I just realized I can simply implement __get__ and return a types.MethodType , but I don't really understand how doing so still enables you to call empty_function.

It's because MethodType has a __getattribute__ method that delegates unknown attributes to its im_func :

>>> t.dosomething
<bound method Test.? of <__main__.Test object at 0x107639550>>
>>> 'empty_function' in dir(t.dosomething)
False
>>> t.dosomething.__getattribute__
<method-wrapper '__getattribute__' of instancemethod object at 0x109583aa0>
>>> t.dosomething.__getattribute__('empty_function')
<bound method FunctionFaker.empty_function of <__main__.FunctionFaker object at 0x1095f2510>>

Of course in CPython, the C API doesn't exactly mirror the Python-level distinction between __getattribute__ and __getattr__ , so the way it's really implemented is with a custom getattro slot. You can read the details in the source .

Does it simply become an attribute of the MethodType instance?

Yes, but only dynamically, by giving you the attribute of the underlying callable.

I don't think they specifically intended to enable class instances substituting for functions with method descriptors of their own. But this support is needed for even simple cases of attaching attributes to methods. For example, with a standalone function, you can use a function attribute for, eg, a memoization cache, or lazy-initialize-on-first-call setup. If MethodType didn't delegate attribute access to its im_func object, moving such a function into a class would break it, and the developer wouldn't be able to fix it unless he knew how descriptors worked and rewrote the method in an ugly way.

In fact, up to 2.3, methods didn't even have a __dict__ ; as you can see from the source , all attributes except for the C slots were delegated to im_func (by effectively duplicating the normal machinery to delegate everything to im_func but wrap errors). There was some debate about this, which you could probably find by searching the python-dev archives for a post by Christian Tismer in the pre-2.4 period with a relevant-looking subject (it may be this thread , but I didn't read the whole thing…). From 2.4 on, methods now do the normal lookup mechanism (except for the special case of __doc__ ), and only delegate to im_func if it fails.

And is this a sane thing to do?

It's a little strange, and it might be simpler to add the empty_function attribute to the function object, instead of wrapping it in a class… but I don't think it's too unreasonable. (I assume you're asking about your code, not about how MethodType and descriptors are implemented.)

You're on the right track. You want your class to also be a descriptor :

import types

class FunctionFaker( object ):
    def __init__(self, f):
        self.f= f

    def empty_function(self):
        pass

    def __call__(self, *args, **kwargs):
        self.empty_function()
        self.f(*args, **kwargs)

    def __get__(self, instance, cls=None):
        # see https://docs.python.org/2/howto/descriptor.html#functions-and-methods
        return types.MethodType(self, instance, cls)

@FunctionFaker
def foo(arg1):
  print "in foo", arg1

class Bar(object):
  @FunctionFaker
  def bar(self, arg1):
    print "in bar", self, arg1

foo('hello')  # in foo hello
Bar().bar('world')  # in bar <__main__.Bar object at 0x7fc50b90fb10> world
Bar().bar.empty_function()

Now, if your decorator is bound to a class, it behaves like a descriptor (binding the proper instance to self in the decorated function), and if it isn't bound to a class, it behaves as a normal decorator. Neat.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM