深入理解Python异常处理机制:重新学习带来的新收获

前言

前些天,公司准备使用开源BI工具superset,但部署成功后,连接阿里数仓获取表时,一直报错,苦于日志不详细,从日志中并没有看出哪里的问题,然后就拉源码进行调试,终于找到抛出异常的位置,但是当我打印这个异常的时候并没有异常信息输出,这促使我重新看一遍python的异常与错误处理。

try语句

try:
    pass
except Exception as e:
    print(e)
finally:
    print("Finally block")

位置

我们需要把更精确的except语句放到最前面,python的内置异常类之间是存在继承关系的,就拿我看superset源码中的例子

print(issubclass(NotImplementedError, RuntimeError)) # True
print(issubclass(RuntimeError, Exception)) # True
print(issubclass(Exception, BaseException)) # True

可以看到异常类的继承关系:BaseException -> Exception -> RuntimeError -> NotImplementedError,当然,这只是我举的一个例子。

如果代码块中有多条except,异常匹配会按照从上到下的顺序进行。

如果把模糊不清的异常放在前面,就会导致下面的except不会触发,像下面这样

dic = {"name": "panda", "age": 18}
try:
    print(dic['sex'])
except Exception as e:
    print(f'exception: {e}')
except KeyError:
    print('sex not in dic')

这段代码会输出exception: 'sex',KeyError下面的代码永远不会执行。

分支

啥时候需要分支呢?和条件语句的分支一样吗?

当我们需要程序没有异常时,才执行之后的逻辑,我们就需要分支了。

先看看,不使用else,我们是这样实现的

dic = {"name": "panda", "age": 18}
successed = False
try:
    print(dic['sex'])
    successed = True
except KeyError:
    print('sex not in dic')
​
if successed:
    # 业务逻辑
    print('successed')

这里我们定义了一个额外变量,来控制真正的业务逻辑是否执行。如果使用try语句中的else分支,代码会变的很简单,像这样

dic = {"name": "panda", "age": 18}
try:
    dic['name']
except KeyError:
    print('sex not in dic')
else:
    # 业务逻辑
    print('successed')

代码运行会输出successed,虽然和条件语句中的else是同一个词,但意义不同,这里的else表示try语句未抛出异常时,才执行else分支下的内容。

当然,需要注意的是,和finally语句不同,如果程序遇到return或break语句,中断异常捕获,即使没有任何异常,else中的逻辑也不会执行,像这样

def demo():
    try:
        a = 1
        return a
    except Exception as e:
        print(e)
    else:
        print('no error')
    finally:
        print('finally')
​
demo()

这段代码,会输出finally,虽然代码没有异常,但是因为有return,导致else中的代码不会执行。

空raise语句

抛出异常,交给上层处理

def demo():
    try:
        a = 1/0
    except Exception as e:
        raise
demo()

空raise语句,会原封不动的重新抛出当前异常。这段代码执行结果为ZeroDivisionError: division by zero

建议抛出异常不要返回错误

之前写代码,我通常习惯将错误和结果一起返回,就是返回一个元组,包含返回结果和错误信息,然后通过错误信息来判断,进行后面的逻辑,像这样

MAX_LENGTH_OF_NAME = 12
MAX_ITEMS_QUOTA = 10
​
​
class Item:
    def __init__(self, name):
        self.name = name
​
​
def create_item(name):
    """接收名称,创建 Item 对象
​
    :return: (对象,错误信息),成功时错误信息为 ''
    """
    if len(name) > MAX_LENGTH_OF_NAME:
        return None, 'name of item is too long'
    return Item(name=name), ''
​
​
def create_from_input():
    name = input()
    item, err_msg = create_item(name)
    if err_msg:
        print(f'create item failed: {err_msg}')
    else:
        print('item<{name}> created')
​
create_from_input()

create_item()用来创建Item对象,函数内部进行长度判断逻辑,执行失败返回空字符串和错误信息组成的元组,在调用层

create_from_input()来判断是否有错误信息,从而进行不同逻辑处理。刚开始感觉这样写还不错,后来代码量上来,都是这种写法,写的就懵了。我们应该使用异常来进行错误处理,会优雅很多,像这样

# -*- coding: utf-8 -*-
​
MAX_LENGTH_OF_NAME = 12
MAX_ITEMS_QUOTA = 10
​
​
def get_current_items():
    return []
​
​
class Item:
    def __init__(self, name):
        self.name = name
​
​
class CreateItemError(Exception):
    def __init__(self, *args, **kwargs):
        pass
​
​
def create_item(name):
    """创建一个新的 Item
​
    :raises: 当无法创建时抛出 CreateItemError
    """
    if len(name) > MAX_LENGTH_OF_NAME:
        raise CreateItemError('name of item is too long')
    return Item(name=name)
​
​
def create_from_input():
    name = input()
    try:
        item = create_item(name)
    except CreateItemError as e:
        print(f'create item failed: {e}')
    else:
        print(f'item<{name}> created')
​
create_from_input()

这里我们自定义了异常错误类CreateItemError,这样create_item()函数只会返回Item类型或者抛出异常,清晰多了。

with

这并不陌生,我们通常操作一个文件时,会使用with来打开文件,像这样

with open('filename', 'r') as f:
    pass

但并不是所有对象都能配合with使用,需要满足上下文管理器协议的对象才可以。

要满足上下文管理器,需要实现__enter____exit__两个魔法方法,__enter__在进入管理器时被调用,__exit__在退出管理器时调用。

with使用案例

代替finally清理资源,先来看看使用finally是这样实现的

conn = MySQLDB(host, port, user, password, database)
try:
    conn.execute_sql('select * from ...')
except Exception as e:
    print(f'Unable to use connection: {e}')
finally:
    conn.close()
​

这是我举的一个例子,MySQLDB是用来连接数据库的类,实现了execute_sql方法用来执行sql语句,最后关闭连接。

如果使用with会简洁很多,MySQLDB类中实现__enter____exit__两个魔法方法

class MySQLDB(SQLBase):
    """MySQL DB table API"""
​
    def __init__(self, host: str, port: int, user: str, password: str, database: str, charset='utf8mb4'):
        """
        Connect to the MySQL database
        :param host:
        :param port:
        :param user:
        :param password:
        :param database:
        """
        self.connection = pymysql.connect(host=host,
                                          port=int(port),
                                          user=user,
                                          password=password,
                                          database=database,
                                          charset=charset,
                                          cursorclass=pymysql.cursors.DictCursor)
​
    def __enter__(self):
        return self.connection
​
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.connection.close()
        return False
​
    def execute_sql(self, sql: str) -> None:
        """
        Execute SQL
        pass

接下来,我们就可以使用with了

with MySQLDB(host='127.0.0.1', port=3306, user='root', password='123456', database='test') as db:
    db.execute_sql("")

忽略异常,有时候出现异常,并不会影响业务逻辑,但异常会阻碍程序运行,此时就需要try/except来捕获,但如果需要忽略的异常比较多,就有很多try/except,不优雅,我们可以直接实现一个上下文管理器来统一忽略异常,像这样

class ignore_closed:
​
    def __enter__(self):
        pass
​
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type == NotImplementedError:
            return True
        return False

在使用时,如果需要忽略NotImplementedError错误,就可以使用with,with ignore_closed():

__exit__接收三个参数,with上下文内未抛出异常,解释器在执行__exit__方法时,exc_type、exc_value、traceback这三个参数的值都是None,如果有异常抛出,这三个参数就是异常的具体内容:

  • exc_type :异常的类型
  • exc_value:异常对象
  • traceback:错误的堆栈对象

此时,如果__exit__返回True,异常就不会继续抛出,如果返回False,那异常就正常抛出。

当然,标准库模块contextlib里面的suppress函数,提供了忽略异常功能,可以直接使用

contextmanager装饰器

上面的例子可以看到,定义一个上下文管理器还是比较麻烦的,需要实现两个魔法方法。为了简化,python提供的更简便的装饰器:@contextmanager,像这样

from contextlib import contextmanager
​
​
@contextmanager
def create_conn_obj():
    conn = MySQLDB(host='127.0.0.1', port=3306, user='root', password='123456', database='test')
    try:
        yield conn
    finally:
        conn.close()

yield前面的逻辑会进入管理器时执行,就像__enter__,yield后面的逻辑会在退出管理器时执行,就像__exit__

想要在上下文管理器内处理异常,必须使用 try包裹yield语句。

@contextmanager可以把一个生成器函数转化成上下文管理器。

定位问题

看superset源码定位问题时,这样一段代码出现异常

try:
    views = set(inspector.get_view_names(schema))
except Exception as ex:
    print(ex)
    raise cls.get_dbapi_mapped_exception(ex) 

在这段代码中,当发生异常时,通过 print(ex) 语句打印异常信息是一个常见的做法。然而,并非所有类型的异常都包含可读的文本信息。有些异常可能不会直接包含有用的信息,而是需要从其他属性中获取。因此,打印异常时可能看不到具体的错误信息。

为了更好地查看异常信息,可以尝试打印完整的异常堆栈信息,而不仅仅是异常对象本身。可以修改代码如下:

try:
    views = set(inspector.get_view_names(schema))
except Exception as ex:
    import traceback
    traceback.print_exc()
    raise cls.get_dbapi_mapped_exception(ex) 

通过使用 traceback.print_exc() 函数,可以打印完整的异常堆栈信息,包括异常的类型、消息和堆栈轨迹,这样可以更全面地了解发生了什么异常以及其详细信息。学到了,果然很快就解决问题了

最后

如果你对Python感兴趣,想要学习python,这里给大家分享一份Python全套学习资料,都是我自己学习时整理的,希望可以帮到你,一起加油!

😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓

1️⃣零基础入门

① 学习路线

对于从来没有接触过Python的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
在这里插入图片描述

② 路线对应学习视频

还有很多适合0基础入门的学习视频,有了这些视频,轻轻松松上手Python~
在这里插入图片描述

③练习题

每节视频课后,都有对应的练习题哦,可以检验学习成果哈哈!
在这里插入图片描述

2️⃣国内外Python书籍、文档

① 文档和书籍资料

在这里插入图片描述

3️⃣Python工具包+项目源码合集

①Python工具包

学习Python常用的开发软件都在这里了!每个都有详细的安装教程,保证你可以安装成功哦!
在这里插入图片描述

②Python实战案例

光学理论是没用的,要学会跟着一起敲代码,动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。100+实战案例源码等你来拿!
在这里插入图片描述

③Python小游戏源码

如果觉得上面的实战案例有点枯燥,可以试试自己用Python编写小游戏,让你的学习过程中增添一点趣味!
在这里插入图片描述

4️⃣Python面试题

我们学会了Python之后,有了技能就可以出去找工作啦!下面这些面试题是都来自阿里、腾讯、字节等一线互联网大厂,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
在这里插入图片描述
在这里插入图片描述

上述所有资料 ⚡️ ,朋友们如果有需要的,可以扫描下方👇👇👇二维码免费领取🆓

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值