Search code examples
pythonfunctional-programmingmetaprogrammingpython-decorators

Can python decorators be used to change behavior of a function returned by a function?


I have functions, that return validator functions, simple example:

def check_len(n):
    return lambda s: len(s) == n

Is it possible to add a decorator, that prints out a message, in case the check evaluates to false? Something like this:

@log_false_but_how
def check_len(n):
    return lambda s: len(s) == n

check_one = check_len(1)
print(check_one('a')) # returns True
print(check_one('abc')) # return False

Expected output:

True
validator evaluated to False
False

I've tried creating an annotation, but can only access the function creation with it. One way would be to define the functions like this:

def log_false(fn):
    def inner(*args):
        res = fn(*args)
        if not res:
            print("validation failed for {}".format(fn.__name__))
        return res
    return inner


@log_false
def check_one(s):
    return check_len(1)(s)

But this way we lose the dynamic creation of validation functions.


Solution

  • You're doing the validation in the wrong place. check_len is a function factory, so res is not a boolean - it's a function. Your @log_false decorator has to wrap a validator function around each lambda returned by check_len. Basically you need to write a decorator that decorates the returned functions.

    def log_false(validator_factory):
        # We'll create a wrapper for the validator_factory
        # that applies a decorator to each function returned
        # by the factory
    
        def check_result(validator):
            @functools.wraps(validator)
            def wrapper(*args, **kwargs):
                result = validator(*args, **kwargs)
                if not result:
                    name = validator_factory.__name__
                    print('validation failed for {}'.format(name))
                return result
            return wrapper
    
        @functools.wraps(validator_factory)
        def wrapper(*args, **kwargs):
            validator = validator_factory(*args, **kwargs)
            return check_result(validator)
        return wrapper
    

    Result:

    @log_false
    def check_len(n):
        return lambda s: len(s) == n
    
    check_one = check_len(1)
    print(check_one('a')) # prints nothing
    print(check_one('abc')) # prints "validation failed for check_len"