Skip to content

空操作、异常处理、日志

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

在软件开发中,异常处理和日志是两个非常重要的部分,它们对于确保软件的健壮性、可维护性和问题诊断能力至关重要。因此本节的学习,对于我们开发和维护项目十分重要。

空操作

在讲解异常处理、日志之前,我们先来了解一下空操作。所谓“空操作”也就是什么也不做,只是作为语法上的一个占位符。在 Python 中,关键字 pass 就是一个空操作语句,它作用就是省略当前代码不报错,目的为了保持程序结构的完整性,经常用于那些需要语句体但暂时还不想实现任何操作的场合,例举如下:

  1. 在条件语句或循环语句中作为占位符:在某些情况下,你可能需要保留某个条件分支或循环体,但暂时不想在其中执行任何操作。
python
if some_condition:  
    pass  # 注释:当满足某些条件时,暂时不执行任何操作。

for i in range(10):
    if i % 2 == 0:
        continue  # 注释:跳过偶数。
    else:  
        pass      # 注释:对奇数暂时不执行操作。
  1. 定义函数或类时,暂时不实现具体内容:当你开始编写一个函数或类,但还没想好如何实现时,可以使用 pass 来占位,避免语法异常。
python
def my_function():
    pass  # 暂未实现  

class MyClass:  
    pass  # 暂未定义任何属性和方法

注意

虽然 pass 在语法上是必要的,但在实际编程中,应避免过度使用。在一些代码审查或代码质量检查工具中,过度使用 pass 可能会被视为一种潜在的代码异味(code smell),因为它可能意味着代码尚未完成或存在冗余。所以 pass 应该被视为一个临时的解决方案,而不是最终的设计。一旦你有了具体的实现计划,就应该用实际的代码立即替换 pass 语句。

异常处理

在讲解异常处理之前,先来了解一下异常的产生。首先什么是“异常”?所谓“异常”就是解释器在执行程序的过程中遇到无法继续执行的情况(例如除数为零、文件不存在等)。这时解释器会在无法继续执行的地方生成一个异常,并在其附近寻找处理这个异常的代码。如果没有,解释器会将这个异常抛给其调用函数,就这样层层抛出,如果在最后的函数中也没有对这个异常处理的代码,解释器就会将整个程序终止掉,并抛出这个异常异常。作为程序员的我们,肯定不希望自己写的代码在运行时出现异常,但是随着代码体量的增大,出现异常是难以避免的,这时我们就需要学习如何使用“异常处理”了。简单讲,异常处理就是程序异常的一种处理机制。它允许程序优雅地处理这些异常,也就是在代码可能会报错的地方进行特殊处理,让运行流程变得更加可靠,减少因报错或崩溃而异常退出的情况。使用异常处理所带来的直接好处如下:

  • 提高程序的健壮性:通过捕获并处理异常,可以防止程序因为一个小的异常而完全崩溃。
  • 提供更好的用户体验:可以向用户展示友好的异常消息,而不是显示技术性的异常堆栈。
  • 简化调试:通过捕获并记录异常信息,可以更容易地定位问题所在。

异常分类

Python 自身引发的所有异常都是 Exception 的子类实例,而 Exception 是从 BaseException 继承的新式类,其中有三个抽象的子类:

  • ArithmeticError:由于算术异常而引发的异常基类,如:OverflowErrorZeroDivisionErrorFloatingPointError
  • LookupError:容器在接收到一个无效键或索引时引发的异常基类,如:IndexErrorKeyError
  • EnvironmentError:由于外部原因而导致的异常基类,如:IOErrorOSErrorWindowsError
错误类型描述
AssertionError断言语句错误。
AttributeError属性引用或赋值错误。
FloatingPointError浮点型运算错误。
IOErrorI/O 操作失败。
ImportErrorimport 语句不能找到要导入的模块,或者不能找到该模块特别请求的名称。
IndentationError解析器遇到了一个由于错误的缩进而引发的语法错误。
IndexError用来索引序列的整数超出了范围,就是下标错误。
KeyError用来索引映射的键不在映射中。
KeyboardInterrupt用户按了中断键(ctrl+c、ctrl+break 或 delete 键)。
MemoryError爆内存。
NameError用了一个不存在的变量名。
NotImplementedError由抽象基类引发的异常,用于指示一个具体的子类必须覆盖一个方法。
OSError由模块 os 中的函数引发的异常,用来指示平台相关的错误。
OverflowError整数运算的结果太大导致溢出。
SyntaxError语法错误。
SystemErrorpython 本身或某些扩展模块中的内部错误。
TypeError对某对象执行了不支持的操作。
UnboundLocalError引用未绑定值的本地变量。
UnicodeError在 Unicode 的字符串之间进行转换时发生的错误。
ValueError应用于某个对象的操作或函数,这个对象具有正确的类型,但确有不适当的值。
WindowsError模块 os 中的函数引发的异常,用来指示与 Windows 相关的错误。
ZeroDivisionError除数为 0 的错误。

抛出异常

关键字 raise抛出的指定异常。

python
x = 0
if x == 0:
    raise ZeroDivisionError  # 报错:抛出ZeroDivisionError(除数为0)的异常。

关键字 Exception抛出指定的内容。

python
x = 0
if x == 0:
    raise Exception("除数不能为0")  # 报错:抛出一个异常,异常内容为“除数不能为0”。

关键字 assert用于进行调试断言。

  • 使用格式:assert 条件语句, 自定义异常消息

  • 使用方法:当 条件语句 的布尔值为 True 时,程序继续向下执行。当 条件语句 的布尔值为 False 时,就会触发一个 AssertionError 异常并返回自定义异常消息。

python
x = 0
assert x != 0, "x不能等于0!"  # 报错:AssertionError: x不能等于0!注释:条件语句布尔值为False,触发异常。

警告

总结一下,assert 是一种快速失败(fail-fast)机制,也是 Python 中一个非常有用的调试工具。虽然它可以帮助开发者快速发现程序中的错误,但它不应用于生产环境的错误处理。对于生产环境,应该使用适当的错误处理机制,如 try...except 块。

捕获异常

捕获异常涉及关键字有四组,具体如下:

  • 关键字 try尝试执行下面一个缩进的代码块。

  • 关键字 except执行 try 下面的代码块出现异常时,则根据异常类型,执行 except 下面的代码块。

  • 关键字 else执行 try 下面的代码块没有异常时,执行 else 下面的代码块。

  • 关键字 finally执行 try 下面的代码块时,不管是否出现异常,都会执行 finally 下面的代码块。

捕获异常的流程设计有七种,具体如下:

  1. 捕获所有异常:执行代码 1 出现异常时,不管是什么类型的异常,都会执行代码 2。
  2. 捕获代码执行异常:执行代码 1 出现异常时,且不属于控制程序流程的异常时,才会执行代码 2。
  3. 捕获单个异常:执行代码 1 出现异常,且异常类型属于指定的类型,才会执行代码 2。
  4. 捕获多个异常一起处理:执行代码 1 出现异常,且异常类型在数组范围内,才会执行代码 2。
  5. 捕获多个异常分别处理:执行代码 1 出现异常,根据异常的类型,执行代码 2 或代码 3。
  6. 无异常处理:执行代码 1 没有异常出现,才会执行代码 3。
  7. 有无异常处理流程:无论代码 1 执行是否出现异常,都会执行代码 4。
python
try:
    代码1
except:
    代码2
'''
执行流程1:执行代码1 ——> 没有异常 ——> 程序结束
执行流程2:执行代码1 ——> 出现异常 ——> 执行代码2 ——> 程序结束
'''
python
try:
    代码1
except Exception as e:  # 注释:将捕获到异常重命名为变量e,可以使用print进行输出。
    代码2
'''
执行流程1:执行代码1 ——> 没有异常 ——> 程序结束
执行流程2:执行代码1 ——> 出现异常 ——> 执行代码2 ——> 程序结束
'''
python
try:
    代码1
except 异常类型 as e:
    代码2
'''
执行流程1:执行代码1 ——> 没有异常 ——> 程序结束
执行流程2:执行代码1 ——> 出现异常 ——> 不是指定的异常类型 ——> 程序结束
执行流程3:执行代码1 ——> 出现异常 ——> 是指定的异常类型 ——> 执行代码2 ——> 程序结束
'''
python
try:
    代码1
except (异常类型1, 异常类型2...) as e:  # 注释:多个异常放在同一个except子句中,用圆括号括起来,并用逗号分隔。
    代码2
'''
执行流程1:执行代码1 ——> 没有异常 ——> 程序结束
执行流程2:执行代码1 ——> 出现异常 ——> 不在数组异常类型中 ——> 程序结束
执行流程3:执行代码1 ——> 出现异常 ——> 在数组异常类型中 ——> 执行代码2 ——> 程序结束
'''
python
try:
    代码1
except 异常类型1 as e:
    代码2
except 异常类型2 as e:
    代码3
'''
执行流程1:执行代码1 ——> 没有异常 ——> 程序结束
执行流程2:执行代码1 ——> 出现异常 ——> 属于异常类型1 ——> 执行代码2 ——> 程序结束
执行流程3:执行代码1 ——> 出现异常 ——> 属于异常类型2 ——> 执行代码3 ——> 程序结束
执行流程4:执行代码1 ——> 出现异常 ——> 不属于异常类型1或异常类型2 ——> 程序结束
'''
python
try:
    代码1
except:
    代码2
else:
    代码3
'''
执行流程1:执行代码1 ——> 出现异常 ——> 执行代码2 ——> 程序结束
执行流程2:执行代码1 ——> 没有异常 ——> 执行代码3 ——> 程序结束
'''
python
try:
    代码1
except:
    代码2
else:
    代码3
finally:
    代码4
'''
执行流程1:执行代码1 ——> 出现异常 ——> 执行代码2 ——> 执行代码4 ——> 程序结束
执行流程2:执行代码1 ——> 没有异常 ——> 执行代码3 ——> 执行代码4 ——> 程序结束
'''

警告

需要注意,直接使用 except: 虽然可以捕获所有的异常,但它可能会隐藏你真正想要解决的异常,因此通常不推荐。另外,使用 except Exception as e 不会捕获由 SystemExitKeyboardInterruptGeneratorExit 触发的用于控制程序流程的异常。

执行优先级

首先我们写一个普通的捕获异常案例如下:程序先执行了 try 块中的代码出现了报错,而报错类型和 except 所捕获类型相同,所以执行了 except 块中的代码进行了输出,最后执行了 finally 块中的代码。可以看到,程序的整个执行过程是从上往下依次执行的,这说明 tryexceptfinally 这三个关键字的执行优先级是一样的。

python
try:
    print({'a': 'b', 'b': 1}['c'])  # 报错:前面字典中没有c键,因此键异常。
except KeyError:                    # 注释:指定捕获的异常类型为键异常。
    print('键异常')
finally:
    print('异常捕获结束')             # 注释:无论try中的代码是否报错这里都会输出。
'''
输出:
键异常
异常捕获结束
'''

现在我们将这个例子写成一个函数如下:程序在 except 捕获到相同类型的异常后,先执行了里面的 print 语句,然后就去执行了 finally 块中的 print 语句,最后又返回来执行了 except 块中的 return 语句结束了函数并返回了后面的值。造成下面程序输出顺序的原因在于,当一个函数中的 try-except-finally 结构中包含 return 语句时,那么 finally 块中的整个代码将优先于 try 块的 return 语句和 except 块的 return 语句执行。

python
def test():
    try:
        print({'a': 'b', 'b': 1}['c'])
        return 1
    except KeyError:
        print('键异常')
        return 2
    finally:
        print('异常捕获结束')

test = test()
print(test)
'''
输出:
键异常
异常捕获结束
2
'''

最后我们将上面例子修改如下:程序在 except 捕获到相同类型的异常后,先执行了里面的 print 语句,然后就去执行了 finally 块中的 print 语句,接着执行了下面一行 return 语句结束了函数并返回了后面的值。

python
def test():
    try:
        print({'a': 'b', 'b': 1}['c'])  # 报错:前面字典中没有c键,因此键异常。
        return 1
    except KeyError:                    # 注释:指定捕获的异常类型为键异常。
        print('键异常')                  # 输出:键异常。注释:报错类型和捕获类型相同,所以进行了输出。
        return 2                        # 注释:这里return会结束函数,因此finally块的代码将优先执行。
    finally:
        print('异常捕获结束')             # 输出:异常捕获结束
        return 3                        # 注释:结束掉整个函数,不会再回到except块中执行return语句了。

test = test()
print(test)
'''
输出:
键异常
异常捕获结束
3
'''

日志

一个项目从开发到上线一般都会经历开发环境和生产环境,简单介绍如下:

  • 开发环境:即开发程序的本地环境。在程序开发中难免都会遇到一些问题,在本地环境下,我们可以使用 Debug 调试功能来排查程序执行异常的位置,从而提升程序的开发效率。
  • 生产环境:即运行程序的线上环境。开发者所开发的程序最终都是要放到线上服务器去运行的,在线上环境下,我们只能看到程序的运行效果,不能直接看到代码每一步的运行过程,相当于是在一个黑盒环境下运行的。

现在如果程序在线上环境出现问题,在没有其他任何信息的情况下,那么只能根据问题的现象来复现,而且大概率是无法准确复现的,这个时候日志的重要性就凸显出来了。日志就是保存了代码运行过程中相关的时间、状态、异常等记录的以 .log 为后缀名的文本文件。通过查看日志,我们能就知道在程序运行的过程中出现了怎样的状况,从而可以快速排查问题

提醒

任何一款软件如果没有日志记录,都不能算作一个合格的软件。作为开发者,我们需要重视并做好日志记录。

logging 标准库

在开发 Python 程序的过程中,很多人会使用 print 输出一些运行信息,然后再根据信息定位到文件中具体的代码,这样做其实是非常不规范的。在 Python 中有一个内置的专门用来进行日志记录的 logging 模块,它相比 print 有这么几个优点:

  • logging 可以输出信息到任意位置,如写入文件、写入远程服务器等,而 print 输出信息都会到标准输出流中。
  • logging 具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用。
  • logging 可以设置日志等级,在开发环境或生产环境中,设置不同的等级来记录对应的日志,而 print 输出的信息无法设置等级。

日志等级

日志级别的指定通常都是在应用程序的配置文件中进行指定的,只有级别大于或等于该指定日志级别的日志记录才会被输出,小于该等级的日志记录将会被丢弃,即等级越高日志信息量越少。当程序部署到生产环境中时,通过指定日志等级,只记录下程序异常、异常等信息,这样来减小服务器的 I/O 压力,也避免了我们在排查故障时被淹没在日志的海洋里。

日志等级(level)描述函数函数说明
DEBUG最详细的日志信息,典型应用场景:问题诊断。logging.debug(msg)创建一条DEBUG级别的日志记录
INFO信息详细程度仅次于DEBUG,通常只记录关键节点信息,用于确认一切都是按照我们预期的那样进行工作。logging.info(msg)创建一条INFO级别的日志记录
警告当某些不期望的事情发生时记录的信息(如,磁盘可用空间较低),但是此时应用程序还是正常运行的。logging.warning(msg)创建一条警告级别的日志记录
ERROR由于一个更严重的问题导致某些功能不能正常运行时记录的信息。logging.error(msg)创建一条ERROR级别的日志记录
CRITICAL当发生严重错误,导致应用程序不能继续运行时记录的信息。logging.critical(msg)创建一条CRITICAL级别的日志记录

建议

在开发应用程序或部署开发环境时,使用 DEBUG 或 INFO 级别的日志获取尽可能详细的日志信息来进行开发或部署调试。在应用上线或部署生产环境时,使用 警告、ERROR 或 CRITICAL 级别的日志来降低机器的 I/O 压力,提高获取异常日志信息的效率。

日志参数

**通过 logging.basicConfig() 方法把设置的内容以参数的形式传递进去就可以自定义日志了。**该函数可接收的关键字参数如下:

参数名称描述
filename指定日志输出目标文件的文件名,指定后日志就不会被输出到控制台了。
filemode指定日志文件的打开模式,默认为 a,该选项要在 filename 指定时才有效。
format指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。
datefmt指定日期/时间格式。需要注意的是,该选项要在 format 中包含时间字段 %(asctime)s 时才有效。
level指定日志器的日志级别
stream指定日志输出目标 stream,如 sys.stdoutsys.stderr 以及网络 stream。需要说明的是,streamfilename 不能同时提供,否则会引发 ValueError 异常。
style指定 format 格式字符串的风格,可取值为 %{$,默认为 %
handlers该选项如果被指定,它应该是一个创建了多个 handler 的可迭代对象,这些 handler 将会被添加到 root logger。需要说明的是:filenamestreamhandlers 这三个配置项只能有一个存在,不能同时出现 2 个或 3 个,否则会引发 ValueError 异常。

上面表格中的 format 参数的格式字符串如下:

名称格式描述
%(asctime)s日志事件发生的时间--人类可读时间,如:2003-07-08 16:49:45,896
%(created)f日志事件发生的时间--时间戳,就是当时调用 time.time() 函数返回的值
%(relativeCreated)d日志事件发生的时间相对于 logging 模块加载时间的相对毫秒数(目前还不知道干嘛用的)
%(msecs)d日志事件发生事件的毫秒部分
%(levelname)s该日志记录的文字形式的日志级别(DEBUGINFO警告ERRORCRITICAL
%(levelno)s该日志记录的数字形式的日志级别(1020304050
%(name)s所使用的日志器名称,默认是 root,因为默认使用的是 rootLogger
%(message)s日志记录的文本内容,通过 msg % args计算得到的
%(pathname)s调用日志记录函数的源码文件的全路径
%(filename)spathname 的文件名部分,包含文件后缀
%(module)sfilename 的名称部分,不包含后缀
%(lineno)d调用日志记录函数的源代码所在的行号
%(funcName)s调用日志记录函数的函数名
%(process)d进程 ID
%(processName)s进程名称
%(thread)d线程 ID
%(thread)s线程名称

日志案例

python
# 引入logging模块
import logging

# 日志等级(默认等级是logging.警告)
LOG_LEVEL = logging.INFO
# 日志输出格式(默认格式是BASIC_FORMAT内容:%(levelname)s:%(name)s:%(message)s)
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(filename)s[:%(lineno)d] - %(message)s"
# 日志输出路径
LOG_FILE = 'info.log'
# 传入上面配置
logging.basicConfig(level=LOG_LEVEL, format=LOG_FORMAT, filename=LOG_FILE)
logging.debug("This is a debug log.")
logging.info("This is a info log.")
logging.warning("This is a warning log.")
logging.error("This is a error log.")
logging.critical("This is a critical log.")
'''
当前路径下会生成一个名称为'info.log'的日志文件,该文件中的内容为:
2023-02-10 16:57:38,112 - INFO - test_persion.py[:143] - This is a info log.
2023-02-10 16:57:38,112 - 警告 - test_persion.py[:144] - This is a warning log.
2023-02-10 16:57:38,112 - ERROR - test_persion.py[:145] - This is a error log.
2023-02-10 16:57:38,112 - CRITICAL - test_persion.py[:146] - This is a critical log.
注释:因为设置的日志级别是INFO,因此只有小于它的DEBUG级别的日志记录被丢弃了,其他级别的日志都输出了。
'''

Loguru 三方库

虽然 logging 已经满足了记录日志的需求,但是在配置上比较繁杂,而且在一些多线程、多进程的场景下使用还会出现日志记录混乱的情况。但有这么一个库,它不仅没有 logging 繁杂的配置还能实现和 logging 类似的功能,同时还能保证日志记录线程进程的安全,这就是第三库 Loguru

提醒

第三库 Loguru 的名字来自于印度语,含义为“日志大师”

安装使用

首先来安装 Loguru 第三方库,执行如下命令:

pip install loguru

接着进行一个简单的日志输出,代码如下:可以看到下图中带颜色的输出结果,这是为了使用方便,loguru 提前预置的输出样式。

python
# Loguru库设计的初衷就在于只追求一个logger
from loguru import logger

# 日志输出到终端
logger.debug("msg msg msg!")

QQ截图20230212212006

日志等级

Loguru 一共有七个日志等级,依此是 tracedebuginfosuccesswarningerrorcritical,严重程度逐渐增加。具体使用如下:

python
from loguru import logger

logger.trace('trace msg')  # 注释:日志级别过低,不会输出。
logger.debug('debug msg')
logger.info('info msg')
logger.success('success msg')
logger.warning('warning msg')
logger.error('error msg')
logger.critical('critical msg')

另外 Loguru 的日志输出还特别的优雅,将其和 logging 日志输出进行对比,以下分别是终端上日志输出:在默认情况下,logging 输出所有等级的日志均是一样的颜色,而 Loguru 输出的日志会根据不同等级配置不同的颜色

465307d890b5b3613e35acbbbd923f30

4e91fd7944901e5f74009a3e18cf22ba

日志配置

Loguru 中几乎所有的操作几乎都围绕一个 logger 对象进行。例如,在上面我们通过 logger 对象的 add 方法设置了日志的级别,不仅如此 add 方法还可以自定义日志输出样式、过滤输出信息等配置。另外,通过 logger 对象调用 remove 方法默认会移除所有的配置,当然也可以通过配置的 handler_id 移除指定的配置。具体使用案例如下:

  1. 写入文件:使用 add 方法设置将日志信息保存到指定的文件。另外,在文件名中添加 {time}{time:YYYY-MM-DD} 参数,可以将当前时间添加到日志文件名称中。
  2. 移除配置:由于将日志信息输出到 Console 终端是 Loguru 的一个默认配置,因此在默认情况下,所有的日志信息都会输出到 Console 终端。如果不想将日志信息输出到 Console 终端,只想保存到文件,就需要在调用 add 方法前调用 remove 方法来移除默认配置即可。
  3. 保存等级:在 Loguru 中保存日志的默认等级是 DEBUG,也就说低于 DEBUG 等级的 TRACE 等级日志就不会保存。如果想修改日志的保存等级可以设置 add 方法中的 level 参数,这样低于该等级的日志就不会保存了。需要注意,日志信息只是不保存,如果没有调用 remove 方法,低于该等级的日志还是会输出在 Console 终端。
  4. 输入格式:如果觉得 Loguru 的默认日志输出格式不满意,我们还可以设置 add 方法中的 format 参数进行更改。
  5. 文件划分:在默认情况下,所以的日志都是保存在一个文件当中,但这样会造成单个日志文件体积过大,在查看日志时就很不方便。这时就可以设置 add 方法中的 rotation 参数,按规则用多个文件保存日志。注意,日志的划分规则只能一个
  6. 保留时间:在默认情况,所有的日志文件都是永久保存的,如果没有定时清理,一些时间久远的无用日志就会占用一部分存储空间,因此日志保留时间的设置是很有必要的。这时就可以设置 add 方法中的 retention 参数,指定日志文件的保留时间(从日志文件创建时开始计时)。注意,retention 参数必须结合上面的 rotation 参数一起使用
  7. 压缩格式:在需要压缩日志的场景中,可以设置 add 方法中的 compression 参数,指定日志的压缩格式(支持:gz、bz2、xz、lzma、tar、tar.gz、tar.bz2、tar.xz)。
  8. 线程安全:在高并发场景中,各线程或进程可能同时写入日志文件,容易导致竞争条件和数据错乱。这时就可以设置 add 方法中的 enqueue 参数,用于控制是否在一个独立的线程中写入日志。启用 enqueue=True 可以在多线程或多进程环境下更安全地记录日志,避免并发写入可能导致的文件冲突或数据丢失。
python
from loguru import logger

# 选择以下其中一个add方法,其他注释掉
logger.add("msg.log")  # 注释:将日志保存到当前路径的msg.log文件
logger.add("msg_{time}.log")  # 注释:精确当前时间到毫秒添加到日志文件名称中, 例如:msg_2023-02-13_14-19-41_896796.log。
logger.add("msg_{time:YYYY-MM-DD}.log") # 注释:精确当前时间到日期添加到日志文件名称中, 例如:msg_2023-02-13.log。

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
from loguru import logger

# 使用remove方法,移除所有配置(包括默认配置)
logger.remove()
logger.add("msg_{time}.log")

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
from loguru import logger

logger.remove()
# 设置日志的保存等级为ERROR
logger.add("msg_{time}.log", level="ERROR")

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
from loguru import logger

logger.remove()
# 设置日志的输出格式为{time} - {level} - {message}
logger.add("msg_{time}.log", format="{time} - {level} - {message}")

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
from loguru import logger
from datetime import timedelta

logger.remove()
# 选择以下其中一个rotation规则,其他注释掉
logger.add("msg_{time}.log", rotation="500 MB")  # 注释:文件超过500M重新生成一个日志文件。
logger.add("msg_{time}.log", rotation="1 week")  # 注释:每过一周生成一个日志文件。
logger.add("msg_{time}.log", rotation="12:00")   # 注释:每天12点生成一个日志文件。
logger.add("msg_{time}.log", rotation=timedelta(minutes=5))  # 注释:每隔5分钟生成一个日志文件。
logger.add("msg_{time}.log", rotation=timedelta(seconds=1))  # 注释:每隔1秒钟生成一个日志文件。

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
from loguru import logger
from datetime import timedelta

logger.remove()
# 选择以下其中一个retention规则,其他注释掉
logger.add("msg_{time}.log", rotation="500 MB", retention="7 days")  # 注释:日志超过500MB创建一个日志,日志保留时间为7天。
logger.add("msg_{time}.log", rotation="12:00", retention=timedelta(hours=1))  # 注释:每天12点创建一个日志,日志保留时间为1小时。
logger.add("msg_{time}.log", rotation=timedelta(seconds=5), retention=timedelta(seconds=10))  # 注释:每隔5秒创建一个日志,日志保留时间为10秒。

while True:
    time.sleep(1)
    logger.info('info msg')
    logger.warning('warning msg')
    logger.error('error msg')
python
from loguru import logger

# 设置日志的保存格式为zip
logger.add("msg_{time}.log", compression="zip")    

logger.info('info msg')
logger.warning('warning msg')
logger.error('error msg')
python
# enqueue支持多进程/异步(默认线程安全)
logger.add("msg_{time}.log", enqueue=True)  # 注释:支持多进程/异步记录日志。

日志捕获

当程序发生异常的时候,我们可以使用 add 方法中的 exception 方法来记录下整个函数栈的信息。例如,下面这个捕获除 0 的异常代码:

python
from loguru import logger

try:
    1 / 0
except:
    # exception方法可以记录下完整的异常栈。
    logger.exception("get exception")  
'''
输出:
> File "C:/test_person.py", line 870, in <module>
    1 / 0

ZeroDivisionError: division by zero
'''

除了使用 exception 方法手动记录异常,还可以使用 catch 方法自动记录异常。例如,下面这个捕获除 0 的异常代码:

python
from loguru import logger

# catch方法可以指定捕获的异常类型和日志等级,默认捕获所有的异常。
with logger.catch(ZeroDivisionError, level="警告"):  
    1 / 0
'''
输出:
> File "C:/test_person.py", line 870, in <module>
    1 / 0

ZeroDivisionError: division by zero
'''

除了使用 catch 自动记录异常,还可以使用 catch 当装饰器,直接捕获被装饰函数里面的异常。例如,下面这个捕获除 0 的异常代码:

python
from loguru import logger

@logger.catch
def my_function(x, y, z):
    return 1 / (x + y + z)

my_function(0, 0, 0)
'''
输出:
2021-10-19 15:04:51.675 | ERROR    | __main__:<module>:10 - An error has been caught in function '<module>', process 'MainProcess' (30456), thread 'MainThread' (26268):
Traceback (most recent call last):

> File "D:/python3Project\test.py", line 10, in <module>
    my_function(0, 0, 0)
    └ <function my_function at 0x014CDFA8>

  File "D:/python3Project\test.py", line 7, in my_function
    return 1 / (x + y + z)
                │   │   └ 0
                │   └ 0
                └ 0

ZeroDivisionError: division by zero
'''