Python中用于包装try except的通用装饰器?

75

我需要与很多深度嵌套的json交互,但我并没有编写它们。我想让我的Python脚本更加“宽容”处理无效输入。我发现自己要编写复杂的try-except块,而我更愿意将可疑的函数进行包装。

我知道吞咽异常是一个不好的策略,但我更希望它们被打印出来以便稍后分析,而不是实际停止执行。在我的用例中,继续执行循环比获取所有键更有价值。

这是我现在正在做的:

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'] = ''

这是我想要的东西,(1):

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

或者(2):

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

我希望能够用某个函数来包装单个数据项(1)或主函数(2),使其在执行时如果出现异常就将其转换为空字段,并打印到stdout。前一种情况相当于对每个项目进行跳过,如果该键不可用,则记录空白并继续执行下一个;后一种情况是跳过一整行,如果任何字段无法工作,则跳过整个记录。

我的理解是应该有一种包装器可以解决这个问题。下面是我尝试使用包装器的代码:

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

以下是为什么它不起作用的原因。调用一个不存在的函数,它不会尝试捕获(try-catch):

这里解释了代码无法正常工作的原因。如果调用一个不存在的函数,代码不会将其捕获并尝试处理:

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

在添加空返回值之前,我希望能够正确地使用try-catch。如果该函数有效,那么会打印“Error”,对吧?

在这里使用包装函数的方式正确吗?

更新

下面有很多非常有用和有帮助的答案,谢谢你们——但我已经编辑了上面的示例,以说明我正在尝试捕获更多嵌套的键错误,而且我特别寻找一个用于包装try-catch的函数,用于...

  1. 当方法不存在时。
  2. 当对象不存在时,并且正在调用它的方法。
  3. 当被调用的函数的参数为不存在的对象时。
  4. 任何这些事物的任意组合。
  5. 额外奖励,当函数不存在时。

1
要访问嵌套的JSON,您可能需要查看safeJSON。它通过有效地包装对象myobject来实现。 - BrenBarn
10个回答

60

这里有很多好的答案,但我没有看到任何一个回答到你是否可以通过装饰器来完成。

简短的答案是“不行”,至少不会对代码进行结构性的更改。装饰器操作针对的是函数级别,而不是单个语句。因此,要使用装饰器,您需要将每个要装饰的语句移动到其自己的函数中。

但请注意,你不能仅仅将赋值本身放在被装饰的函数内部。你需要从被装饰的函数返回rhs表达式(要分配的值),然后在外部执行赋值操作。

为了将此应用到你的示例代码中,可能会编写以下模式的代码:

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

item["a"] = computeA()

return_on_failure 可以是这样的:

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

你的代码从未引用传递给装饰器的 value 参数,因此装饰器函数永远不会返回它。在我看来,这没有意义,也不符合你所声称的功能。 - martineau
@MattV:我认为给这个答案额外加分是考虑不周的。 - martineau
我是一个新手,但我认为这个答案有点误导,因为它告诉我们不能用装饰器来实现这个,而在Sergeev Andrew和glglgl的回答中,我们可以看到如何使用装饰器来解决这个问题,并提供了例子。 - Fernando Wittmann

49
你可以使用defaultdict和Raymond Hettinger在PyCon 2013演讲中概述的上下文管理器方法
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']

嗯,这很不错。我要调查一下。谢谢。 - Mittenchops
链接失效了。有什么想法在哪里可以找到它吗? - jdennison
1
这是一场非常棒的演讲,对我来说真的整合了很多东西。我现在会将它与《像 Python 程序员一样编码:Python 惯用法》(http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html)一起推荐。谢谢。 - Peter Wood
ignored函数中,我可以使用raise而不是pass,并在主调用中捕获异常以便为相关代码块打印错误消息吗?@iruvar - alper
3
@alper,那样做会有点违背使用“ignored”的初衷,不是吗?如果我理解你的问题正确的话,你想要打印出与手头异常相关的详细信息。你可以这样做- import sys, traceback,然后在ignoredpass之前加上ex_type, ex, tb = sys.exc_info()traceback.print_tb(tb) - iruvar

33

使用可配置的装饰器非常容易实现。

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)

只需传递装饰器元组,其中包含您想要消除的错误类型和默认返回值即可。

输出将为

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

编辑:感谢 martineau 的建议,我将错误的默认值更改为包含基本异常的元组,以防止错误。


抱歉我的知識有所欠缺。我試圖在我的腳本中使用它,但它沒有給我想要的結果。我的錯誤是'AttributeError: 'NoneType' object has no attribute text',這是由於f(soup.find("span", class_='xxx').text)引起的。我將裝飾器定義為'f = get_decorator(errors=(AttributeError,), default_value="#NA")'。我在這裡做錯了什麼? - MattV
@MVersteeg:你需要将@f应用于返回soup.find("span", class_='xxx').text表达式的值的函数,该表达式导致了异常——正如答案中所示的示例。 - martineau
已点赞,尽管我会将 get_decorator() 的签名更改为 def get_decorator(default_value='', *errors) 以稍微简化调用它的过程。 - martineau
@martineau 我将错误的默认值更改为防止异常。 - Sergeev Andrew
值得注意的是,特别是对于一次性使用,将 @get_decorator((KeyError, NameError), default_value='default') 放在函数前面也可以起作用。 - martineau

15

14

这取决于您期望的异常情况。

如果您唯一的用例是get(),您可以这样做

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

对于其他情况,您的装饰器方法可能有用,但不是您现在所做的方式。

我会试着给您展示:

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

然而,f(some_undefined_function())不起作用,因为:

a) f()在执行时尚未激活,

b) 它的使用方法是错误的。正确的方式是将函数进行包装,再调用它:f(function_to_wrap)()

在这里,“lambda层”会有帮助:

wrapped_f = f(lambda: my_function())

包装了一个lambda函数,该lambda函数调用一个不存在的函数。调用wrapped_f()会导致调用包装器,该包装器调用lambda函数,lambda函数尝试调用my_function()。如果该函数不存在,则lambda函数引发异常,该异常由包装器捕获。

这是因为在定义lambda函数时并不执行my_function,而是在执行时才执行。然后这个执行被函数f()保护和包装起来。所以异常发生在lambda函数内部,并传播到装饰器提供的包装函数中进行优雅处理。

如果您尝试用包装器替换lambda函数,则此向内移动的步骤将无法工作。

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

跟着一个

f(g(my_function))(arguments)

因为在这里名称解析是“回到表面”:不能解析my_function,并且这发生在调用g()甚至是f()之前。所以它不起作用。

如果您尝试执行类似以下内容的操作

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

如果你没有 xg() 保护的是 print 而不是 x,所以它不能正常工作。

如果你想要保护这里的 x,你需要做

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

由于f()提供的包装器调用了引发异常的lambda函数,然后将其抑制。


这很有帮助,但我还在继续工作。再深入一点,是否有一种方法,而不是用lambda包装,我可以创建另一个函数g(),它可以在通用函数(包括*args和**kwargs)上为我进行lambda包装?这样我就可以调用g(anyfunction()),并且它会产生我想要的完整效果? - Mittenchops
当我尝试运行时,我输入了 >>> wrapped_p = f(lambda *z,**z: func(*z,**z)),但是出现了以下错误信息:File "<stdin>", line 1 SyntaxError: duplicate argument 'z' in function definition - Mittenchops
当然,你可以使用 g = lambda function: lambda *a, **k: function(*a, **k) 这个函数来实现 g(anyfunction)() 的效果。(只是这样:你不想包装函数的结果,而是函数本身。) - glglgl
谢谢,那是我犯傻了。但是即使使用这个函数来包装我的函数,我似乎仍然有同样的问题。所以,>>> g = f(lambda function: lambda *a, **k: function(*a,**k)) 然后我用 >>> g(print)(x.get('fail')) 调用它,得到一个消息说 x 不存在,而不是我要寻找的无声失败。 - Mittenchops
1
@Roylearnstocode 正确,我们将lambda表达式(可调用对象)传递给该函数。在我们的情况下,它处理调用未定义的函数:如果 my_function 不存在,则调用该 lambda 函数将导致异常,可以在内部捕获。如果我们使用不存在的 my_function 执行 f(my_function),则不会调用 f() 并且内部的“异常避免机制”也不会被使用。 - glglgl
显示剩余3条评论

9

在您的情况下,您首先评估喵呼叫的价值(该值不存在),然后将其包装在修饰符中。这样做是行不通的。

首先,在包装之前会引发异常,然后包装器缩进错误(silenceit 不应返回自身)。您可能希望执行以下操作:

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

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

softfail =f(hardfail)

输出:

>>> 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

无论如何,在您的情况下,我不明白为什么您不使用简单的方法,例如

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

并且在代码中:

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

编辑:

如果您希望功能适用于任何深度,请可以进行以下操作:

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

你需要打电话:

>>> 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}

使用您的代码
item['a'] = get_from_object(obj, 2, 3) 

顺便说一下,就个人观点而言,我也喜欢@cravoori使用contextmanager的解决方案。但这意味着每次需要三行代码:

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

谢谢!把Return放错位置修正了我以为我在做的但实际上并不是。但是我仍然不知道为什么meow()中的内部异常在wrap之前被调用了。我不能使用简单的方法的原因是,我正在多个不同深度上调用它,使用具有不同属性的多个不同对象。这将使我为每个分配编写一个函数,这将像try-catch一样繁琐。所以,我正在寻找可以通用地捕获失败的功能并返回'',打印错误到stdout的东西。 - Mittenchops

4

2

由于你正在处理大量破损的代码,因此在这种情况下使用eval可能是可以原谅的。

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

然后将所有可能出错的语句进行包裹:
item['a'] = my_eval("""myobject.get('key').get('subkey')""")
item['b'] = my_eval("""myobject.get('key2')""")
item['c'] = my_eval("""func1(myobject)""")

3
永远不要在任何编程语言中使用 eval。 - Camille Tolsa

2
为什么不能直接使用循环?
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] = ''

或者,如果您希望编写一个小助手:

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

如果你有几个需要获取值的地方,而且帮助函数更加合理,你也可以将这两种解决方案结合起来。

不确定你是否真的需要一个装饰器来解决你的问题。


我正在尝试使其更通用,不仅适用于一级深度缺失的键,而且还可以处理许多函数因各种原因无法分配数据的情况。 - Mittenchops

-2

用于同步和异步函数的Try Except Decorator

注意: logger.error 可以被替换为 print

最新版本可以在这里找到。

enter image description here


6
请勿使用图片代替代码。 - logi-kal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接