karlosss
karlosss

Reputation: 3165

Extending behavior of the property decorator

I would like to extend the behavior of the builtin @property decorator. The desired usage is shown in the code below:

class A:
    def __init__(self):
        self.xy = 42

    @my_property(some_arg="some_value")
    def x(self):
        return self.xy

print(A().x) # should print 42

First of all, the decorator should retain the property behavior so that no () is needed after the x. Next, I would like to be able to access the arguments a programmer passes to my decorator.

I started off with this:

class my_property(property):
   def __init__(self, fn):
       super().__init__(fn)

TypeError: __init__() got an unexpected keyword argument 'some_arg'

After adding **kwargs:

class my_property(property):
   def __init__(self, fn, **kwargs):
       super().__init__(fn)

TypeError: __init__() missing 1 required positional argument: 'fn'

OK, let's do *args instead:

class my_property(property):
   def __init__(self, *args, **kwargs):
       super().__init__(*args)

TypeError: 'my_property' object is not callable

Let's make it callable:

class my_property(property):
    def __init__(self, *args, **kwargs):
        super().__init__(*args)

    def __call__(self, *args, **kwargs):
        pass

No errors, but prints None instead of 42

And now I am lost. I have not even yet managed to access `some_arg="some_value" and the property behavior seems to be already gone. What is wrong and how to fix it?

Upvotes: 0

Views: 45

Answers (1)

VersBersch
VersBersch

Reputation: 193

It's not clear how you intent to use some_arg, but to pass a parameter to a decorator you need to have "two layers" of decorators

@my_decorator(arg)
def foo():
    return

under the hood this translates to my_decorator(arg)(foo) (i.e. my_decorator(arg) must return another decorator that is called with foo). The inner decorator in this case should be your custom implementation of property

def my_property(some_arg):
    class inner(object):
        def __init__(self, func):
            print(some_arg)  # do something with some_arg
            self.func = func

        def __get__(self, obj, type_=None):
            return self.func(obj)
    return inner

Now you can use it like this:

class MyClass:
    def __init__(self, x):
        self.x = x

    @my_property('test!')
    def foo(self):
        return self.x


obj = MyClass(42)  # > test!
obj.foo            # > 42  

Read more about descriptors here

Upvotes: 1

Related Questions