如何改进 python/django 中的异常处理

Asa*_*ara 5 python django exception

这是我在 Django 项目中的异常处理示例:

def boxinfo(request, url: str):
    box = get_box(url)
    try:
        box.connect()
    except requests.exceptions.ConnectionError as e:
        context = {'error_message': 'Could not connect to your box because the host is unknown.'}
        return render(request, 'box/error.html', context)
    except requests.exceptions.RequestException as e:
        context = {'error_message': 'Could not connect to your box because of an unknown error.'}
        return render(request, 'box/error.html', context)
Run Code Online (Sandbox Code Playgroud)
  • 现在只有两个例外,但对于几个请求例外应该更多。但是视图方法已经被这个膨胀了。有没有办法将异常处理转发到单独的错误方法?
  • 还有一个问题,我需要在这里为每个除外调用渲染消息,我想避免这种情况。
  • 在这里我也重复除了“无法连接到你的盒子因为”之外的每一个,当出现任何异常时应该设置一次。

我可以通过这样的方式解决它:

try:
    box.connect()
except Exception as e:
    return error_handling(request, e)
Run Code Online (Sandbox Code Playgroud)

——

def error_handling(request, e):
    if type(e).__name__ == requests.exceptions.ConnectionError.__name__:
        context = {'error_message': 'Could not connect to your box because the host is unknown.'}
    elif type(e).__name__ == requests.exceptions.RequestException.__name__:
        context = {'error_message': 'Could not connect to your box because of an unknown error.'}
    else:
        context = {'error_message': 'There was an unkown error, sorry.'}
    return render(request, 'box/error.html', context)
Run Code Online (Sandbox Code Playgroud)

然后我当然可以改进错误消息。但总的来说,它是一种处理异常的pythonic方式if/else吗?例如RequestException,如果ConnectionError抛出,我无法在这里捕获,所以我需要捕获每个请求错误,这看起来更像是一个丑陋的摆弄......

Kev*_*sco 10

This is a use case for decorators. If it's something more general that applies to all views (say, error logging), you can use the Django exception middleware hook, but that doesn't seem to be the case here.

With respect to the repetitive error string problem, the Pythonic way to solve it is to have a constant base string with {replaceable_parts} inserted, so that later on you can .format() them.

With this, say we have the following file decorators.py:

import functools

from django.shortcuts import render
from requests.exceptions import ConnectionError, RequestException


BASE_ERROR_MESSAGE = 'Could not connect to your box because {error_reason}'


def handle_view_exception(func):
    """Decorator for handling exceptions."""
    @functools.wraps(func)
    def wrapper(request, *args, **kwargs):
        try:
            response = func(request, *args, **kwargs)
        except RequestException as e:
            error_reason = 'of an unknown error.'
            if isinstance(e, ConnectionError):
                error_reason = 'the host is unknown.'
            context = {
              'error_message': BASE_ERROR_MESSAGE.format(error_reason=error_reason),
            }
            response = render(request, 'box/error.html', context)
        return response

    return wrapper
Run Code Online (Sandbox Code Playgroud)

We're using the fact that ConnectionError is a subclass of RequestException in the requests library. We could also do a dictionary with the exception classes as keys, but the issue here is that this won't handle exception class inheritance, which is the kind of omission that generates subtle bugs later on. The isinstance function is a more reliable way of doing this check.

If your exception tree keeps growing, you can keep adding if statements. In case that starts to get unwieldy, I recommend looking here, but I'd say it's a code smell to have that much branching in error handling.

Then in your views:

from .decorators import handle_view_exception

@handle_view_exception
def boxinfo(request, url: str):
    box = get_box(url)
    box.connect()
    ...
Run Code Online (Sandbox Code Playgroud)

That way the error handling logic is completely separate from your views, and best of all, it's reusable.