75

I'd interacting with a lot of deeply nested json I didn't write, and would like to make my python script more 'forgiving' to invalid input. I find myself writing involved try-except blocks, and would rather just wrap the dubious function up.

I understand it's a bad policy to swallow exceptions, but I'd rather prefer they to be printed and analysed later, than to actually stop execution. It's more valuable, in my use-case to continue executing over the loop than to get all keys.

Here's what I'm doing now:

try:
    item['a'] = myobject.get('key').METHOD_THAT_DOESNT_EXIST()
except:
    item['a'] = ''
try:
    item['b'] = OBJECT_THAT_DOESNT_EXIST.get('key2')
except:
    item['b'] = ''
try:
    item['c'] = func1(ARGUMENT_THAT_DOESNT_EXIST)
except:
    item['c'] = ''
...
try:
    item['z'] = FUNCTION_THAT_DOESNT_EXIST(myobject.method())
except:
    item['z'] = ''

Here's what I'd like, (1):

item['a'] = f(myobject.get('key').get('subkey'))
item['b'] = f(myobject.get('key2'))
item['c'] = f(func1(myobject)
...

or (2):

@f
def get_stuff():
   item={}
   item['a'] = myobject.get('key').get('subkey')
   item['b'] = myobject.get('key2')
   item['c'] = func1(myobject)
   ...
   return(item)

...where I can wrap either the single data item (1), or a master function (2), in some function that turns execution-halting exceptions into empty fields, printed to stdout. The former would be sort of an item-wise skip - where that key isn't available, it logs blank and moves on - the latter is a row-skip, where if any of the fields don't work, the entire record is skipped.

My understanding is that some kind of wrapper should be able to fix this. Here's what I tried, with a wrapper:

def f(func):
   def silenceit():
      try:
         func(*args,**kwargs)
      except:
         print('Error')
      return(silenceit)

Here's why it doesn't work. Call a function that doesn't exist, it doesn't try-catch it away:

>>> f(meow())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'meow' is not defined

Before I even add a blank return value, I'd like to get it to try-catch correctly. If the function had worked, this would have printed "Error", right?

Is a wrapper function the correct approach here?

UPDATE

I've had a lot of really useful, helpful answers below, and thank you for them---but I've edited the examples I used above to illustrate that I'm trying to catch more than nested key errors, that I'm looking specifically for a function that wraps a try-catch for...

  1. When a method doesn't exist.
  2. When an object doesn't exist, and is getting a method called on it.
  3. When an object that does not exist is being called as an argument to a function.
  4. Any combination of any of these things.
  5. Bonus, when a function doesn't exist.
Mittenchops
  • 18,633
  • 33
  • 128
  • 246
  • 1
    For accessing nested JSON specifically, you might want to look at [safeJSON](https://github.com/NYTimes/safejson). This works by effectively wrapping the object `myobject`. – BrenBarn Dec 14 '14 at 21:20

10 Answers10

56

There are lots of good answers here, but I didn't see any that address the question of whether you can accomplish this via decorators.

The short answer is "no," at least not without structural changes to your code. Decorators operate at the function level, not on individual statements. Therefore, in order to use decorators, you would need to move each of the statements to be decorated into its own function.

But note that you can't just put the assignment itself inside the decorated function. You need to return the rhs expression (the value to be assigned) from the decorated function, then do the assignment outside.

To put this in terms of your example code, one might write code with the following pattern:

@return_on_failure('')
def computeA():
    item['a'] = myobject.get('key').METHOD_THAT_DOESNT_EXIST()

item["a"] = computeA()

return_on_failure could be something like:

def return_on_failure(value):
  def decorate(f):
    def applicator(*args, **kwargs):
      try:
         return f(*args,**kwargs)
      except:
         print('Error')
         return value

    return applicator

  return decorate
FBruzzesi
  • 6,385
  • 3
  • 15
  • 37
Nathan Davis
  • 5,636
  • 27
  • 39
  • Your code never references the `value` argument passed to the decorator so the decorator function will never return it. IMO it makes no sense and does not do what you claim. – martineau Aug 19 '21 at 18:35
  • @MattV: I think awarding extra points to this answer was ill-considered. – martineau Aug 19 '21 at 18:40
  • I'm new to decorators, but I think this answer feels a bit misleading to me as it is telling that we can't accomplish this with decorators, while in Sergeev Andrew and glglgl's responses, we can see how to solve this using decorator along with examples – Fernando Wittmann Jan 04 '23 at 15:01
49

You could use a defaultdict and the context manager approach as outlined in Raymond Hettinger's PyCon 2013 presentation

from collections import defaultdict
from contextlib import contextmanager

@contextmanager
def ignored(*exceptions):
  try:
    yield
  except exceptions:
    pass 

item = defaultdict(str)

obj = dict()
with ignored(Exception):
  item['a'] = obj.get(2).get(3) 

print item['a']

obj[2] = dict()
obj[2][3] = 4

with ignored(Exception):
  item['a'] = obj.get(2).get(3) 

print item['a']
iruvar
  • 22,736
  • 7
  • 53
  • 82
  • Hmm, that's neat. I'm going to investigate this. Thanks. – Mittenchops Mar 24 '13 at 15:38
  • link is down. any ideas where to find it? – jdennison Sep 24 '13 at 16:16
  • 1
    That's a great talk, really brought a lot of things together for me. I'll now recommend this along with [Code Like a Pythonista: Idiomatic Python](http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html). Thanks. – Peter Wood Sep 30 '14 at 17:14
  • In `ignored` function instead of using `pass` can I use `raise` and catch an exception on the main call in order to print an error message for the related code block? @iruvar – alper Apr 07 '20 at 12:50
  • 3
    @alper, that would kind of defeat the point of using `ignored`, wouldn't it. If I understand your question you seek to print details correspond to the exception at hand. You could do this instead - `import sys, traceback` and then add `ex_type, ex, tb = sys.exc_info()` followed by `traceback.print_tb(tb)` just before the `pass` in `ignored` – iruvar Apr 07 '20 at 17:03
33

It's very easy to achieve using configurable decorator.

def get_decorator(errors=(Exception, ), default_value=''):

    def decorator(func):

        def new_func(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except errors, e:
                print "Got error! ", repr(e)
                return default_value

        return new_func

    return decorator

f = get_decorator((KeyError, NameError), default_value='default')
a = {}

@f
def example1(a):
    return a['b']

@f
def example2(a):
    return doesnt_exist()

print example1(a)
print example2(a)

Just pass to get_decorator tuples with error types which you want to silence and default value to return. Output will be

Got error!  KeyError('b',)
default
Got error!  NameError("global name 'doesnt_exist' is not defined",)
default

Edit: Thanks to martineau i changed default value of errors to tuples with basic Exception to prevents errors.

Sergeev Andrew
  • 463
  • 4
  • 7
  • Forgive me for a lack of knowledge here. I'm attempting to use it in my script, but it's not giving me the result I'm looking for. My error is 'AttributeError: 'NoneType' object has no attribute text' resulting from f(soup.find("span", class_='xxx').text). I'm defining the decorator as 'f = get_decorator(errors=(AttributeError,), default_value="#NA")'. What am I doing wrong here? – MattV Dec 12 '14 at 17:02
  • @MVersteeg: You need to apply `@f` to a function that returns the value of the `soup.find("span", class_='xxx').text` expression that's generating the exception -- as shown in the examples in the answer. – martineau Dec 13 '14 at 17:59
  • Upvoted, although I'd change the signature of `get_decorator()` to `def get_decorator(default_value='', *errors)` to simplify calling it slightly. – martineau Dec 13 '14 at 18:44
  • @martineau i changed errors default value to prevent exceptions – Sergeev Andrew Dec 15 '14 at 06:12
  • It's worth noting that -- especially for one time usage -- that `@get_decorator((KeyError, NameError), default_value='default')` before the function would also work. – martineau Dec 15 '14 at 08:47
14

It depends on what exceptions you expect.

If your only use case is get(), you could do

item['b'] = myobject.get('key2', '')

For the other cases, your decorator approach might be useful, but not in the way you do it.

I'll try to show you:

def f(func):
   def silenceit(*args, **kwargs): # takes all kinds of arguments
      try:
         return func(*args, **kwargs) # returns func's result
      except Exeption, e:
         print('Error:', e)
         return e # not the best way, maybe we'd better return None
                  # or a wrapper object containing e.
  return silenceit # on the correct level

Nevertheless, f(some_undefined_function())won't work, because

a) f() isn't yet active at the execution time and

b) it is used wrong. The right way would be to wrap the function and then call it: f(function_to_wrap)().

A "layer of lambda" would help here:

wrapped_f = f(lambda: my_function())

wraps a lambda function which in turn calls a non-existing function. Calling wrapped_f() leads to calling the wrapper which calls the lambda which tries to call my_function(). If this doesn't exist, the lambda raises an exception which is caught by the wrapper.

This works because the name my_function is not executed at the time the lambda is defined, but when it is executed. And this execution is protected and wrapped by the function f() then. So the exception occurs inside the lambda and is propagated to the wrapping function provided by the decorator, which handles it gracefully.

This move towards inside the lambda function doesn't work if you try to replace the lambda function with a wrapper like

g = lambda function: lambda *a, **k: function(*a, **k)

followed by a

f(g(my_function))(arguments)

because here the name resolution is "back at the surface": my_function cannot be resolved and this happens before g() or even f() are called. So it doesn't work.

And if you try to do something like

g(print)(x.get('fail'))

it cannot work as well if you have no x, because g() protects print, not x.

If you want to protect x here, you'll have to do

value = f(lambda: x.get('fail'))

because the wrapper provided by f() calls that lambda function which raises an exception which is then silenced.

glglgl
  • 89,107
  • 13
  • 149
  • 217
  • This was helpful, but I'm still working on it. (Exception is misspelled above, but got it.) Taking this a little farther, is there a way that, instead of wrapping with lambda, I could make another function, g() that does that lambda wrap for me on a generic function, including *args, and **kwargs? So that I could call g(anyfunction()) and it would have the full effect I'm looking for? – Mittenchops Mar 24 '13 at 15:35
  • When I tried it, I did `>>> wrapped_p = f(lambda *z,**z: func(*z,**z))` and got `File "", line 1 SyntaxError: duplicate argument 'z' in function definition` – Mittenchops Mar 24 '13 at 15:50
  • Of course; you could do a `g = lambda function: lambda *a, **k: function(*a, **k)` which enables you to do `g(anyfunction)()`. (Only in this way: you don't want to wrap the function result, but the function itself.) – glglgl Mar 24 '13 at 18:55
  • Thanks, that was silly of me. But even using this function to wrap my function, I seem to have the same problem. So, `>>> g = f(lambda function: lambda *a, **k: function(*a,**k))` Then I call it with `>>> g(print)(x.get('fail'))` and get a message that x doesn't exist, rather than the silent failure I'm looking for. – Mittenchops Mar 24 '13 at 19:45
  • Phew, of course. That was my fault. In this context it cannot work because of the time where name resolution occurs. It has to be moved away from immediate execution. I'll elaborate in my answer above. – glglgl Mar 24 '13 at 20:11
  • @glglgl, thanks for your solution, it works like a charm for me. However I don't really understand the syntax here wrapped_f = f(lambda: my_function()), as in "a layer of lambda"... so we are passing in a lambda into f()? but somehow the underlining function is my_function()? By using "lambda: a_function()", we can wrap it as a lambda? – Roy learns to code Oct 28 '22 at 11:32
  • 1
    @Roylearnstocode Exactly, we pass a lambda (which is a callable object) to that function. In our case, that handles calling an undefined function: if `my_function` doesn't exist, calling that lambda function will lead to an exception which can be cought inside. If we would do `f(my_function)` with `my_function`not existing, `f()` wouldn't be called and the "exception avoiding mechanism" inside wouldn't be used. – glglgl Oct 28 '22 at 11:47
  • @glglgl, OMG I didn't expect a 9 years old thread will get answer back so quickly. Thank you so much for your explanation, I get it now. ;-) I was trying the pass in a function A() into a function B(), where B() has a try:... except: I couldn't get the exception cause by A() raised properly in B() without your lambda wrapper. Hugs from Japan! – Roy learns to code Oct 28 '22 at 12:15
14

Extending @iruvar answer - starting with Python 3.4 there is an existing context manager for this in Python standard lib: https://docs.python.org/3/library/contextlib.html#contextlib.suppress

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

with suppress(FileNotFoundError):
    os.remove('someotherfile.tmp')
Pax0r
  • 2,324
  • 2
  • 31
  • 49
9

in your case you first evaluate the value of the meow call (which doesn't exist) and then wrap it in the decorator. this doesn't work that way.

first the exception is raised before it was wrapped, then the wrapper is wrongly indented (silenceit should not return itself). You might want to do something like:

def hardfail():
  return meow() # meow doesn't exist

def f(func):
  def wrapper():
    try:
      func()
    except:
      print 'error'
  return wrapper

softfail =f(hardfail)

output:

>>> softfail()
error

>>> hardfail()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in hardfail
NameError: global name 'meow' is not defined

anyway in your case I don't understand why you don't use a simple method such as

def get_subkey(obj, key, subkey):
  try:
    return obj.get(key).get(subkey, '')
  except AttributeError:
    return ''

and in the code:

 item['a'] = get_subkey(myobject, 'key', 'subkey')

Edited:

In case you want something that will work at any depth. You can do something like:

def get_from_object(obj, *keys):
  try:
    value = obj
    for k in keys:
        value = value.get(k)
    return value
  except AttributeError:
    return ''

That you'd call:

>>> d = {1:{2:{3:{4:5}}}}
>>> get_from_object(d, 1, 2, 3, 4)
5
>>> get_from_object(d, 1, 2, 7)
''
>>> get_from_object(d, 1, 2, 3, 4, 5, 6, 7)
''
>>> get_from_object(d, 1, 2, 3)
{4: 5}

And using your code

item['a'] = get_from_object(obj, 2, 3) 

By the way, on a personal point of view I also like @cravoori solution using contextmanager. But this would mean having three lines of code each time:

item['a'] = ''
with ignored(AttributeError):
  item['a'] = obj.get(2).get(3) 
RickyA
  • 15,465
  • 5
  • 71
  • 95
astreal
  • 3,383
  • 21
  • 34
  • Thanks! The return in the wrong place fixes what I thought I was doing, but wasn't. But I'm still at a loss in how it the internal exception from meow() is called before the wrap. The reason I can't use the simple method is that I am calling at several different depths, using several different objects with different attributes. I'd be writing a function per assignment, which would be as cumbersome as just try-catching. So, I'm looking for something that can generically catch failed functions and return '', printing an error to stdout. – Mittenchops Mar 22 '13 at 17:33
3

How about something like this:

def exception_handler(func):
def inner_function(*args, **kwargs):
    try:
        func(*args, **kwargs)
    except TypeError:
        print(f"{func.__name__} error")
return inner_function

then

@exception_handler
def doSomethingExceptional():
    a=2/0

all credits go to:https://medium.com/swlh/handling-exceptions-in-python-a-cleaner-way-using-decorators-fae22aa0abec

Max
  • 4,152
  • 4
  • 36
  • 52
2

Why not just use cycle?

for dst_key, src_key in (('a', 'key'), ('b', 'key2')):
    try:
        item[dst_key] = myobject.get(src_key).get('subkey')
    except Exception:  # or KeyError?
        item[dst_key] = ''

Or if you wish write a little helper:

def get_value(obj, key):
    try:
        return obj.get(key).get('subkey')
    except Exception:
        return ''

Also you can combine both solutions if you have a few places where you need to get value and helper function would be more reasonable.

Not sure that you actually need a decorator for your problem.

simplylizz
  • 1,686
  • 14
  • 28
  • I'm trying to make it more generic, not just for one-level deep missing keys, but with a lot of functions that can fail to assign data for a lot of reasons. – Mittenchops Mar 22 '13 at 14:53
2

Since you're dealing with lots of broken code, it may be excusable to use eval in this case.

def my_eval(code):
  try:
    return eval(code)
  except:  # Can catch more specific exceptions here.
    return ''

Then wrap all your potentially broken statements:

item['a'] = my_eval("""myobject.get('key').get('subkey')""")
item['b'] = my_eval("""myobject.get('key2')""")
item['c'] = my_eval("""func1(myobject)""")
Addison
  • 1,065
  • 12
  • 17
-1

Try Except Decorator for sync and async functions

Note: logger.error can be replaced with print

Latest version can be found here.

enter image description here

vineet
  • 850
  • 7
  • 9