14

I have a chain of functions, all defined elsewhere in the class:

fus(roh(dah(inp)))

where inp is either a dictionary, or bool(False).

The desired result is that if inp, or any of the functions evaluate to False, False is returned by the function stack.

I attempted to use ternary operators, but they don't evaluate correctly.

def func(inp):
    return int(inp['value']) + 1 if inp else False

throws a TypeError, bool not subscriptable, if i == False because inp['value'] is evaluated before the conditional.

I know I can do it explicitly:

def func(inp):
    if inp == False:
        return False
    else:
        return inp['value'] + 1

but there are a ton of functions, and this will nearly quadruple the length of my code. It's also rewriting the exact same lines of code again and again, which suggests to me that it is the wrong way to do things.

I suspect that a decorator with arguments is the answer, but the more I play around with it the less sure I am about that.

def validate_inp(inp):
    def decorator(func):
        def wrapper(*args):
             return func(inp) if inp else False
        return wrapper
    return decorator

@validate_inp(inp)
def func(inp):
    return int(inp['value']) + 1

Unfortunately the decorator call throws a NameError, 'inp' not defined. But I'm not sure if I'm using the decorator incorrectly, or the decorator is the wrong solution.

Looking for comment, criticism, suggestion, and/or sanity check.


If you found this trying to solve your own problem...

You probably want to be using empty dictionaries instead of boolean False. Props to @chepner.

In my application, using False was "okay" but offered no advantages and caused some chunky blocks of code.

I've found everything is simpler using an empty dictionary instead. I'm wrapping the functions that use the dict with a decorator that catches the KeyError thrown by referencing dict['value'] where dict is empty.

Community
  • 1
  • 1
Jeffrey Swan
  • 537
  • 2
  • 8
  • 26
  • 1
    Do you control `inp`? You know that an empty dict is falsy? – dawg May 07 '15 at 17:14
  • Would it help to just switch the order of the func's return? Just do return False if not inp else return int(inp['value']) + 1 – fatalaccidents May 07 '15 at 17:15
  • Yes, input is always a valid dict or boolean False. – Jeffrey Swan May 07 '15 at 17:16
  • @fatalaccidents Nice catch on a simple solution, but it still throws TypeError: 'bool' object is not subscriptable when inp == False – Jeffrey Swan May 07 '15 at 17:21
  • 2
    Could you return an empty dictionary instead of `False`? At least then, you can concentrate on writing functions that always work on dictionaries. – chepner May 07 '15 at 17:21
  • 1
    @chepner god, I was just about to comment "looking for the Maybe monad aren't you?" :) – enobayram May 07 '15 at 17:22
  • 2
    @Jefe are you sure that `inp` is `False` when it raises exception? Try to print `inp` in the first line of your function. – KAdot May 07 '15 at 17:24
  • 1
    This question may be of interest: http://stackoverflow.com/q/8507200/1126841 – chepner May 07 '15 at 17:39
  • `bool(False)` is the same as just `False`, btw. `False` already is a `bool`, so applying `bool` to it does nothing. – Stefan Pochmann May 07 '15 at 18:24
  • @StefanPochmann Understood. I was being explicit so no one thought I was using the string "False". – Jeffrey Swan May 07 '15 at 18:24
  • 2
    `return int(inp['value']) + 1 if inp else False` should work, since `x if y else z` only evaluates `x` or `z` if that side of the expression is selected. You have some other problem. – user2357112 May 08 '15 at 07:10

6 Answers6

9

Decorator should look like:

def validate_inp(fun):
    def wrapper(inp):
        return fun(inp) if inp else False
    return wrapper


@validate_inp
def func(inp):
    return int(inp['value']) + 1

print(func(False))
print(func({'value': 1}))

If you want to use your decorator with a class member:

def validate_inp(fun):
    def wrapper(self, inp):
        return fun(self, inp) if inp else False
    return wrapper

class Foo(object):
    @validate_inp
    def func(self, inp):
        return int(inp['value']) + 1 if inp else False

foo = Foo()
print(foo.func(False))
print(foo.func({'value': 1}))
KAdot
  • 1,997
  • 13
  • 21
  • Any suggestion on implementing this in a class? If defined in class as validate_inp(self, fun) I get a missing argument type error and passing the function explicitly to the decorator call makes no sense, nor does it work. On the other hand, if I define it outside the class as validate_inp(fun) I get a "take 1 argument, 2 given" type error. – Jeffrey Swan May 07 '15 at 17:47
  • I've also tried defining in the class as validate_inp(fun) and defining wrapper as wrapper(self, inp), but then I just get the same missing argument type error, this time from the wrapped function. – Jeffrey Swan May 07 '15 at 17:54
  • @Jefe I added an example how to use decorator with a class member. – KAdot May 07 '15 at 18:00
  • I appreciate the help. It appears the decorator breaks when implemented in a class like that. The wrapped function correctly evaluates a valid dict, but returns a TypeError, boolean not subscriptable when passed False. The decorator seems to have no effect on the function. – Jeffrey Swan May 07 '15 at 18:06
  • Does it return `TypeError` for the exact code above? – KAdot May 07 '15 at 18:08
  • Triple checked, and the deocrator is defined exactly as in your post. When used to wrap a function that calls input['value'] and input == bool(False): "TypeError: 'bool' object is not subscriptable". It was definitely working in IDLE though. – Jeffrey Swan May 07 '15 at 18:11
  • @Jefe could you provide minimal test code which raises `TypeError`? – KAdot May 07 '15 at 18:12
  • The code is for an API wrapper, so my unittest class setup calls for a key and login credentials, otherwise I'd just share the class and tests. I'll get something together though. – Jeffrey Swan May 07 '15 at 18:15
  • The problem must be something else. The following class implementation of your wrapper evaluates input as desired. http://hastebin.com/esuculagaz.py Thanks again for your help. – Jeffrey Swan May 07 '15 at 18:22
  • @thefourtheye Yes, both you and KAdot have given me the correct answer. The problem is that my mocked up question deviated from my actual requirement in a subtle yet important way. I'm working on figuring out the appropriate changes to the wrapper now. – Jeffrey Swan May 07 '15 at 18:43
8

I attempted to use ternary operators, but they don't evaluate correctly.

def func(inp):
    return int(inp['value']) + 1 if inp else False

throws a TypeError, bool not subscriptable, if i == False because inp['value'] is evaluated before the conditional.

This is not true - that code works. Further, you can just write

def func(inp):
    return inp and (int(inp['value']) + 1)

To automatically wrap functions like this, make a function that wraps a function:

def fallthrough_on_false(function):
    def inner(inp):
        return inp and function(inp)
    return inner

This should be improved by using functools.wraps to carry through decorators and names, and it should probably take a variadic number of arguments to allow for optional extensions:

from functools import wraps

def fallthrough_on_false(function):
    @wraps(function)
    def inner(inp, *args, **kwargs):
        return inp and function(inp, *args, **kwargs)
    return inner
Veedrac
  • 58,273
  • 15
  • 112
  • 169
  • Thanks for the input. I've been using ternaries to great effect for a while now, but it looks like I need to go back and look at some flexibility I missed. And thanks for bringing up functool.wraps. Totally forgot about that, just like every other time I write a decorator. – Jeffrey Swan May 07 '15 at 20:03
4

Unless you are passing a value directly to the decorator, you should not parameterize it. In your case, the inp is actually passed to the function, not to the decorator. So, the implementation becomes like this

>>> def validate_inp(f):
...     def wrapper(inp):
...          if not inp:
...              return False
...          return f(inp)
...     return wrapper
... 
>>> @validate_inp
... def func(inp):
...     return int(inp['value']) + 1
... 
>>> func(False)
False
>>> func({'value': 1})
2

These two lines

@validate_inp
def func(inp):

can be understood like this

func = validate_inp(func)

So, func is actually the wrapper function, returned by validate_inp function. From now on, whenever you are calling func, the wrapper will be invoked, and inp will be passed to wrapper function only. Then wrapper will decide whether to call the actual func or not, based on the value of inp.


If you want to implement the same decorator in a class, you just have to account for the first parameter self in the wrapper function. Thats it.

>>> class Test(object):
... 
...     def validate_inp(fun):
...         def wrapper(self, inp):
...             if not inp:
...                 return False
...             return fun(self, inp)
...         return wrapper
...     
...     @validate_inp
...     def func(self, inp):
...         return int(inp['value']) + 1
...     
... 
>>> Test().func(False)
False
>>> Test().func({'value': 1})
2

Since wrapper is the actual func, it also accepts self and inp. And when you invoke the function f (which is the actual func), you just have to pass on the self as the first parameter.

thefourtheye
  • 233,700
  • 52
  • 457
  • 497
3

This is probably not what you were looking for but do these help your cause ?

1. Use dictionary.get instead of []. Here you can define a fall-back value. For example.

In [1548]: inp
Out[1548]: {'6': 'Hi'}

In [1549]: inp.get('5',99)
Out[1549]: 99
  1. isinstance can be used to check if the variable is a dictionary.

    In [1550]: isinstance(inp, dict)
    Out[1550]: True
    

Putting them together (inp is the same dictionary as above)

In [1554]: print "True" if isinstance(inp, dict) and len(inp.keys()) else "False"
True
Grijesh Chauhan
  • 57,103
  • 20
  • 141
  • 208
fixxxer
  • 15,568
  • 15
  • 58
  • 76
2

One option might be to define a custom exception and a small wrapper:

class FalseInput(Exception): pass
def assert_not_false(inp):
    # I'll assume `inp` has to be precisely False,
    # and not something falsy like an empty dictionary.
    if inp is False:
        raise FalseInput
    return inp

Modify each of your functions to raise the same exception instead of return False. Then, just catch the exception once, at the top of the call stack, but wrap the input first.

try:
    x = fus(roh(dah(assert_not_false(inp))))
except FalseInput:
   x = False

This might be more efficient as well, since you won't necessarily need to call all the functions; if inp starts as False, assert_not_false will raise the exception immediately and you'll jump straight to the except clause.

chepner
  • 497,756
  • 71
  • 530
  • 681
2

Just another option: A helper function that takes a starting value and functions and applies the functions as long as it doesn't encounter False:

def apply(x, *functions):
    for func in functions:
        if x is False:
            return x      # or break
        x = func(x)
    return x

outp = apply(inp, dah, roh, fus)
Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107