Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accessing self in a function attribute

I'm trying to add a decorator that adds callable attributes to functions that return slightly different objects than the return value of the function, but will execute the function at some point.

The problem I'm running into is that when the function object is passed into the decorator, it is unbound and doesn't contain the implicit self argument. When I call the created attribute function (ie. string()), I don't have access to self and can't pass it into the original function.

def deco(func):
    """
    Add an attribute to the function takes the same arguments as the
    function but modifies the output.
    """
    def string(*args, **kwargs):
        return str(func(*args, **kwargs))
    func.string = string
    return func


class Test(object):

    def __init__(self, value):
        self._value = 1

    @deco
    def plus(self, n):
        return self._value + n

When I go to execute the attribute created by the decorator, this is the error I get, because args doesn't contain the self reference.

>>> t = Test(100)
>>> t.plus(1)         # Gets passed self implicitly
101
>>> t.plus.string(1)  # Does not get passed self implicitly
...
TypeError: plus() takes exactly 2 arguments (1 given)

Is there a way to create a decorator like this that can get a reference to self? Or is there a way to bind the added attribute function (string()) so that it also gets called with the implicit self argument?

like image 508
Brendan Abel Avatar asked Oct 21 '16 21:10

Brendan Abel


2 Answers

You can use descriptors here:

class deco(object):

    def __init__(self, func):
        self.func = func
        self.parent_obj = None

    def __get__(self, obj, type=None):
        self.parent_obj = obj
        return self

    def __call__(self, *args, **kwargs):
        return self.func(self.parent_obj, *args, **kwargs)

    def string(self, *args, **kwargs):
        return str(self(*args, **kwargs))


class Test(object):

    def __init__(self, value):
        self._value = value

    @deco
    def plus(self, n):
        return self._value + n

so that:

>>> test = Test(3)
>>> test.plus(1)
4
>>> test.plus.string(1)
'4'

This warrants an explanation. deco is a decorator, but it is also a descriptor. A descriptor is an object that defines alternative behavior that is to be invoked when the object is looked up as an attribute of its parent. Interestingly, bounds methods are themselves implemented using the descriptor protocol

That's a mouthful. Let's look at what happens when we run the example code. First, when we define the plus method, we apply the deco decorator. Now normally we see functions as decorators, and the return value of the function is the decorated result. Here we are using a class as a decorator. As a result, Test.plus isn't a function, but rather an instance of the deco type. This instance contains a reference to the plus function that we wish to wrap.

The deco class has a __call__ method that allows instances of it to act like functions. This implementation simply passes the arguments given to the plus function it has a reference to. Note that the first argument will be the reference to the Test instance.

The tricky part comes in implementing test.plus.string(1). To do this, we need a reference to the test instance of which the plus instance is an attribute. To accomplish this, we use the descriptor protocol. That is, we define a __get__ method which will be invoked whenever the deco instance is accessed as an attribute of some parent class instance. When this happens, it stores the parent object inside itself. Then we can simply implement plus.string as a method on the deco class, and use the reference to the parent object stored within the deco instance to get at the test instance to which plus belongs.

This is a lot of magic, so here's a disclaimer: Though this looks cool, it's probably not a great idea to implement something like this.

like image 163
jme Avatar answered Oct 06 '22 05:10

jme


You need to decorate your function at instantiation time (before creating the instance method). You can do this by overriding the __new__ method:

class Test(object):

    def __new__(cls, *args_, **kwargs_):
        def deco(func):
            def string(*args, **kwargs):
                return "my_str is :" + str(func(*args, **kwargs))
            # *1
            func.__func__.string = string
            return func

        obj = object.__new__(cls, *args_, **kwargs_)
        setattr(obj, 'plus', deco(getattr(obj, 'plus')))
        return obj

    def __init__(self, value):
        self._value = 1

    def plus(self, n):
        return self._value + n

Demo:

>>> t = Test(100)
>>> t.plus(1)
>>> t.plus.string(5)
>>> 'my_str is :6'

1. Since python doesn't let you access the real instance attribute at setting time you can use __func__ method in order to access the real function object of the instance method.

like image 36
Mazdak Avatar answered Oct 06 '22 07:10

Mazdak