SRobProsc
SRobProsc

Reputation: 23

Conditional property decorator on class method

I have a python dataclass in which I want to conditionally assign certain decorators depending on some global variable.

The condition is checked at the top of the script, but for my example below, I've simply supplied the result of that checking. If the check is True, I want to give those methods the @functools.cached_property decorator. If it is False, I just want them to receive the standard @property decorator.

The issue I keep running into is that I can't quite figure out how (or if it's even possible) to make this work as a simple decorator. I mostly get errors about method objects when calling or manipulating test.x_times_y, and I'm not sure if it is possible to write the function in such a way that calling test.x_times_y in the example below actually yields the result that I want.

import functools
import dataclasses

_value_checked = False


def myDecorator(func):
    def decorator(self):
        if not _value_checked:
            return property(func)(self)
        else:
            return functools.cached_property(func)(self)

    return decorator


@dataclasses.dataclass
class MyClass():
    x: int
    y: int
    z: int = 0

    @myDecorator
    def x_times_y(self):
        return self.x*self.y


test = MyClass(5,6,7)

I'd also like to avoid getter and setter methods, so I'm hopeful that that is possible. I've looked at many answers on here (such as this one) but haven't been able to find an answers that actually works, as most don't apply to decorating methods. I'm using Python 3.8 for this.

Upvotes: 2

Views: 645

Answers (2)

Brian61354270
Brian61354270

Reputation: 14434

The behavior you want can be implemented with a simple conditional assignment:

my_decorator = functools.cached_property if _value_checked else property

or

if _value_checked:
    my_decorator = functools.cached_property
else:
    my_decorator = property

If you need to do more complex logic at each use of the decorator, you can use a function that returns the decorator you want:

def my_decorator():
    if not _value_checked:
        return property
    else
        return functools.cached_property

No complex argument forwarding required. Just delegate to the decorators you already have.

Upvotes: 3

Samwise
Samwise

Reputation: 71517

The way you've written myDecorator it can only be applied to functions that take a single argument:

def myDecorator(func):
    def decorator(self):
        if not _value_checked:
            return property(func)(self)
        else:
            return functools.cached_property(func)(self)

    return decorator

The simplest thing is to just return the function and not call it inside a wrapper:

def myDecorator(func):
    if not _value_checked:
        return property(func)
    else
        return functools.cached_property(func)

If you did need to build a wrapper, the generally correct way is to have the wrapper function take arbitrary *args, **kwargs arguments so you can invoke the wrapped function with them:

def myDecorator(func):
    def wrapper(*args, **kwargs):
        if not _value_checked:
            return property(func)(*args, **kwargs)
        else:
            return functools.cached_property(func)(*args, **kwargs)

    return wrapper

Note that the function that myDecorator returns is not itself a decorator, it's a wrapper that replaces the decorated function -- that's why I've renamed it in the above implementation.

Note also that there is a practical difference between these implementations, which is that the second version (with the wrapper) evaluates _value_checked at the time the function is called, whereas the first version evaluates it at the time the function is defined. If that value is a constant it doesn't matter, but if you want to be able to toggle it at runtime and have the behavior change dynamically, you want the second version.

Upvotes: 0

Related Questions