Search code examples
pythondecoratorpython-decorators

Python, How to add another decorator to filter the output of the existing multi decorators with property in python?


I have 2 existing decorators to function in python, @property and @safe_property. Those decorators can't be changed, and they are part of the code that I have no access.


def safe_property(original_property):
    def wrap(self):
        try:
            return original_property(self)
        except AttributeError as e:
            pass
    return wrap

class MyClass(object):
    def __init__(self):
        pass

    @property
    @safe_property
    def do_func(self):
        print("inside do_func!")
        return [2,3,4]

By calling the function:

a = MyClass()
print(a.do_func)

The output is good for me!:


inside do_func!
[2, 3, 4]

Now, another feature has come and I'm trying to filter out some of the return values of do_func according to an (optional) additional argument. That means that some user could continue work as usual and call:

print(a.do_func)

While others could call with a filter:

print(a.do_func(True))

In order to try this, I created another decorator called my_decorator such as:

def my_decorator(*args, **kwargs):

    print(args)
    print(kwargs)
    def wrapper(*args):
        print(args)
        if args[1] == True:
            return
            # how do I return filter?
        else:
            return #without the filter?
    return wrapper


class MyClass(object):
    def __init__(self):
        pass

    @my_decorator
    @property
    @safe_property
    def do_func(self):
        print("inside do_func!")
        return [2,3,4]

The current output of this functionality is:

(<property object at 0x02AF0090>,)
{}
(<__main__.MyClass object at 0x00BCDBB0>, True)
None

How can I filter only the odd number ** for example,** of the return list from: do_func?

Thanks


Solution

  • You are applying your decorator to the output of the @property decorator. That decorator produces a property() object, not a function. That's because decorators are applied outward from the function definition; see my answer on decorator execution order; so @safe_property is applied first, then @property, then @my_decorator.

    If you wanted to decorate the getter function, place your decorator right above the def statement, it'll be executed first, and whatever your decorator returns will be passed to the safe_property() decorator (which adds its own wrapper function):

    @property
    @safe_property
    @my_decorator
    def do_func(self):
        print("inside do_func!")
        return [2,3,4]
    

    or, seeing as @safe_property also produces a wrapper function that's suitable as a getter function, you can place your decorator in between the @safe_property and @property lines to wrap the wrapper function returned the former:

    @property
    @my_decorator
    @safe_property
    def do_func(self):
        print("inside do_func!")
        return [2,3,4]
    

    Either way, your decorator wrapper is passed the callable to decorate, and should return a replacement. Property getters only take self, your replacement would be called with self too, and no other arguments:

    def my_decorator(func):
        def wrapper(self):  # a replacement getter function, so only self is passed in
            result = func(self)  # call the original getter
            if self.some_flag:  # you can access the instance in the wrapper
                # return only odd values from the getter
                return [i for i in result if i % 2]
            else:
                # otherwise return the values unchanged
                return result
        return wrapper
    

    To place @my_decorator at the top is to decorate a property() object, not a function, so you'd need to specifically handle being passed such an object (you can see how a @property decorator works in an answer I wrote before).

    E.g. you could extract the getter from the property().fget attribute, and then return an appropriate replacement (which would be another property() object):

    def my_decorator(prop):
        getter = prop.fget
        def wrapper(self):  # a replacement getter function, so only self is passed in
            result = getter(self)  # call the original getter, taken from the property
            if self.some_flag:  # you can access the instance in the wrapper
                # return only odd values from the getter
                return [i for i in result if i % 2]
            else:
                # otherwise return the values unchanged
                return result
        # return a new property object, with the wrapper as the getter function
        # and copying across all other property attributes
        return property(wrapper, prop.fset, prop.fdel, prop.doc)
    

    Note that a property getter function will only ever be passed self, there are no other arguments for property getters possible.

    However, handling a property object directly doesn't really have any advantages over placing your decorator one line lower, it only complicates matters by having to add the prop.fget reference and property(...) return value.