用于记录的重复包装函数

时间:2014-04-16 01:55:03

标签: python logging decorator

我正在编写一个需要使用外部库中的类的脚本,做一些 对该类的实例进行操作,然后再重复一些实例。

这样的事情:

import some_library

work_queue = get_items()

for item in work_queue:
  some_object = some_library.SomeClass(item)
  operation_1(some_object)
  # ...
  operation_N(some_object)

但是,循环体中的每个操作都可能引发一些不同的异常。 当这些发生时,我需要记录它们并跳到下一个项目。如果他们加薪 在崩溃之前我需要记录一些意外的异常。

我可以捕获主循环中的所有异常,但这会掩盖它的作用。 所以我发现自己编写了一堆看似类似的包装函数:

def wrapper_op1(some_object):
  try:
    some_object.method_1()
  except (some_library.SomeOtherError, ValueError) as error_message:
    logger.error("Op1 error on {}".format(some_object.friendly_name))
    return False
  except Exception as error_message:
    logger.error("Unknown error during op1 on {} - crashing: {}".format(some_object.friendly_name, error_message))
    raise
  else:
    return True

# Notice there is a different tuple of anticipated exceptions
# and the message formatting is different
def wrapper_opN(some_object):
  try:
    some_function(some_object.some_attr)
  except (RuntimeError, AttributeError) as error_message:
    logger.error("OpN error on {} with {}".format(some_object.friendly_name, some_object.some_attr, error_message))
    return False
  except Exception as error_message:
    logger.error("Unknown error during opN on {} with {} - crashing: {}".(some_object.friendly_name, some_object.some_attr, error_message))
    raise
  else:
    return True

将主循环修改为:

for item in work_queue:
  some_object = some_library.SomeClass(item)
  if not wrapper_op1(some_object):
    continue
  # ...
  if not wrapper_opN(some_object):
    continue

这可以完成这项工作,但感觉就像很多复制和粘贴编程一样 包装纸。什么是伟大的是写一个可以的装饰器功能 做所有尝试...除了......别的东西,所以我可以做:

@ logged_call(known_exception, known_error_message, unknown_error_message)
def wrapper_op1(some_object):
  some_object.method_1()

如果操作成功,包装器将返回True,捕获已知的异常 并以指定的格式记录,并在重新提升之前捕获任何未知的日志记录异常。

但是,我似乎无法理解如何使错误消息发挥作用 - 我可以使用固定字符串来实现:

def logged_call(known_exceptions, s_err, s_fatal):
  def decorate(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
      try:
        f(*args, **kwargs)
      # How to get parameters from args in the message?
      except known_exceptions as error:
        print(s_err.format(error))
        return False
      except Exception as error:
        print(s_fatal.format(error))
        raise
      else:
        return True
    return wrapper
  return decorate

但是,我的错误消息需要获取属于修饰函数的属性。

是否有一些Pythonic方法可以完成这项工作?或者使用不同的模式 在处理may-fail-in-known-way函数时?

1 个答案:

答案 0 :(得分:1)

以下tryblock函数可用于此目的,以实现您的logged_call概念并减少代码总量,假设您有足够的检查来克服装饰器实现。不习惯Python中的函数式编程的人实际上可能发现这比仅仅写出try块更难以理解。像许多事情一样,简单性在旁观者的眼中。

Python 2.7不使用导入。这使用exec语句创建一个以功能模式工作的自定义try块。

def tryblock(tryf, *catchclauses, **otherclauses):
  u'return a general try-catch-else-finally block as a function'
  elsef = otherclauses.get('elsef', None)
  finallyf = otherclauses.get('finallyf', None)
  namespace = {'tryf': tryf, 'elsef': elsef, 'finallyf': finallyf, 'func': []}
  for pair in enumerate(catchclauses):
    namespace['e%s' % (pair[0],)] = pair[1][0]
    namespace['f%s' % (pair[0],)] = pair[1][1]
  source = []
  add = lambda indent, line: source.append(' ' * indent + line)
  add(0, 'def block(*args, **kwargs):')
  add(1,   "u'generated function executing a try block'")
  add(1,   'try:')
  add(2,     '%stryf(*args, **kwargs)' % ('return ' if otherclauses.get('returnbody', elsef is None) else '',))
  for index in xrange(len(catchclauses)):
    add(1, 'except e%s as ex:' % (index,))
    add(2,   'return f%s(ex, *args, **kwargs)' % (index,))
  if elsef is not None:
    add(1,   'else:')
    add(2,     'return elsef(*args, **kwargs)')
  if finallyf is not None:
    add(1,   'finally:')
    add(2,     '%sfinallyf(*args, **kwargs)' % ('return ' if otherclauses.get('returnfinally', False) else '',))
  add(0, 'func.append(block)')
  exec '\n'.join(source) in namespace
  return namespace['func'][0]

这个tryblock函数足以进入公共库,因为它不是特定于检查逻辑的。添加你的logged_call装饰器,实现为(此处导入一个):

import functools
resultof = lambda func: func()  # @ token must be followed by an identifier
@resultof
def logged_call():
  truism = lambda *args, **kwargs: True
  def raisef(ex, *args, **kwargs):
    raise ex
  def impl(exlist, err, fatal):
    return lambda func: \
      functools.wraps(func)(tryblock(func,
          (exlist, lambda ex, *args, **kwargs: err(ex, *args, **kwargs) and False),
          (Exception, lambda ex, *args, **kwargs: fatal(ex, *args, **kwargs) and raisef(ex))),
          elsef=truism)
  return impl  # impl therefore becomes logged_call

使用logged_call实施,您的两个完整性检查如下:

op1check = logged_call((some_library.SomeOtherError, ValueError),
    lambda _, obj: logger.error("Op1 error on {}".format(obj.friendly_name)),
    lambda ex, obj: logger.error("Unknown error during op1 on {} - crashing: {}".format(obj.friendly_name, ex.message)))

opNcheck = logged_call((RuntimeError, AttributeError),
    lambda ex, obj: logger.error("OpN error on {} with {}".format(obj.friendly_name, obj.some_attr, ex.message)),
    lambda ex, obj: logger.error("Unknown error during opN on {} with {} - crashing: {}".format(obj.friendly_name, obj.some_attr, ex.message)))

@op1check
def wrapper_op1(obj):
  return obj.method_1()

@opNcheck
def wrapper_opN(obj):
  return some_function(obj.some_attr)

忽略空白行,这比原始代码更多 compact 10行,但是tryblocklogged_call实现的沉没成本;是否现在更多可读是一个意见问题。

您还可以选择在单独的模块中定义logged_call本身以及从中派生的所有不同装饰器,如果这对您的代码是明智的;因此要多次使用每个派生的装饰器。

通过调整实际检查,您还可以找到更多可以考虑logged_call的逻辑结构。

但是在最糟糕的情况下,每个检查都有其他没有的逻辑,你可能会发现只要像你已经那样写出每个检查,它就更具可读性。这真的取决于。

为了完整性,这是tryblock函数的单元测试:

import examplemodule as ex
from unittest import TestCase
class TestTryblock(TestCase):
  def test_tryblock(self):
    def tryf(a, b):
      if a % 2 == 0:
        raise ValueError
      return a + b
    def proc_ve(ex, a, b):
      self.assertIsInstance(ex, ValueError)
      if a % 3 == 0:
        raise ValueError
      return a + b + 10
    def elsef(a, b):
      return a + b + 20
    def finallyf(a, b):
      return a + b + 30
    block = ex.tryblock(tryf, (ValueError, proc_ve))
    self.assertRaises(ValueError, block, 0, 4)
    self.assertRaises(ValueError, block, 6, 4)
    self.assertEqual([5, 16, 7, 18, 9], map(lambda v: block(v, 4), xrange(1, 6)))
    block = ex.tryblock(tryf, (ValueError, proc_ve), elsef=elsef)
    self.assertEqual([25, 16, 27, 18, 29], map(lambda v: block(v, 4), xrange(1, 6)))
    block = ex.tryblock(tryf, (ValueError, proc_ve), elsef=elsef, returnbody=True)
    self.assertEqual([5, 16, 7, 18, 9], map(lambda v: block(v, 4), xrange(1, 6)))
    block = ex.tryblock(tryf, (ValueError, proc_ve), finallyf=finallyf)
    self.assertEqual([5, 16, 7, 18, 9], map(lambda v: block(v, 4), xrange(1, 6)))
    block = ex.tryblock(tryf, (ValueError, proc_ve), finallyf=finallyf, returnfinally=True)
    self.assertEqual([35, 36, 37, 38, 39], map(lambda v: block(v, 4), xrange(1, 6)))