1. 写在前面
最近在重构之前的后端代码,借着这个机会又重新补充了关于python的一些知识, 学习到了一些高效编写代码的方法和心得,比如构建大项目来讲,要明确捕捉异常机制的重要性, 学会使用try...except..finally
, 要通过日志模块logging监控整个系统的性能,学会把日志输出到文件并了解日志层级,日志模块的一些工作原理,能自定义日志,这样能非常方便的debug服务,学会使用装饰器对关键接口进行时间耗时统计,日志打印等装饰,减少代码的重复性, 学会使用类的类方法,静态方法对一些公用函数进行封装,来增强代码的可维护性, 学会使用文档对函数和参数做注解, 学会函数的可变参数统一代码的风格等等, 这样能使得代码从可读性,可维护性, 灵活性和执行效率上都有一定的提升,写出来的代码也更加优美一些 。 所以把这几天的学习,分模块整理几篇笔记, 这次是从实践的再去补充python的内容,目的是要写出漂亮的python代码,增强代码的可读,可维护,灵活和高效,方便调试和监控。
这一篇文章是模块和包, 这里面会整理python里面用到的比较高效的一些处理模块, 比如collections, loggings, enum, typing
等, 学会巧用这些模块,能让写出来的代码更加漂亮,后面再有新的模块也会补充。
大纲如下:
- 模块和包初识
- 常用包大总结
ok, let's go!
2. 模块和包初识
模块,可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突。这样可以提高代码的可维护性和重用性。
python
# 模块导入的方式
# 1. import 模块名1 [as 别名1], 模块名2 [as 别名2],...,导入指定模块中的所有成员(包括变量、函数、类等),当需要使用模块中的成员时,需用该模块名(或别名)作为前缀
# 2. from 模块名 import 成员名1 [as 别名1],成员名2 [as 别名2],..., 只会导入模块中指定的成员,而不是全部成员。同时,当程序中使用该成员时,无需附加任何前缀,直接使用成员名(或别名)即可
import pandas as pd
from sys import argv
# 不建议
from xxx import *
# 因为它存在潜在的风险。比如同时导入 module1 和 module2 内的所有成员,假如这两个模块内都有一个 foo() 函数,如果代码中调用foo(),是执行哪个呢?
# 如果是含有空格或者数字开头的模块, 上面这样导入会语法错误,需要__import__()函数导入
# 比如有一个"demo text.py"的一个文件
import demo text # 语法错误
__import__("demo text") # 需要这样导入
__import__("1demo")
# 注意,使用 __import__() 函数引入模块名时,要以字符串的方式将模块名引入,否则会报 SyntaxError 错误
# 导入模块的本质
# 使用"import xxx"导入模块的本质就是,将xxx.py 中的全部代码加载到内存并执行,然后将整个模块内容赋值给与模块同名的变量,该变量的类型是 module,而在该模块中定义的所有程序单元都相当于该 module 对象的成员
# 使用"from xxx import xxx, xxx"导入模块中成员的本质就是将 xxx.py 中的全部代码加载到内存并执行,然后只导入指定变量、函数等成员单元,并不会将整个模块导入
# 假设写了一个fk_module.py文件
'一个简单的测试模块: fk_module'
print("this is fk_module")
name = 'fkit'
def hello():
print("Hello, Python")
# 下面在test.py中导入
import fk_module
print("================") # 之前会先输出一个 this is fk_module
# 打印fk_module的类型
print(type(fk_module)) # <class 'module'>
print(fk_module) # <module 'fk_module' from 'C:\\Users\\mengma\\Desktop\\fk_module.py'>
from fk_module import name, hello
print("================") # 之前会先输出一个 this is fk_module 说明也是会把xxx.py加载带内存并执行, 但只会导入模块中部分成员了
print(name) # fkit
print(hello) # <function hello at 0x0000000001E7BAE8>
# 打印fk_module
print(fk_module) # NameError: name 'fk_module' is not defined
# 在导入模块后,可以在模块文件所在目录下看到一个名为"__pycache__"的文件夹,打开该文件夹,可以看到 Python 为每个模块都生成一个 *.cpython-36.pyc 文件,比如 Python 为 fk_module 模块生成一个 fk_ module.cpython-36.pyc 文件,该文件其实是 Python 为模块编译生成的字节码,用于提升该模块的运行效率。
# python智能之处: 导入同一个模块多次,Python只执行一次
# 当我们向文件导入某个模块时,导入的是该模块中那些名称不以下划线(单下划线"_"或者双下划线"__")开头的变量、函数和类。
# 因此,如果我们不想模块文件中的某个成员被引入到其它文件中使用,可以在其名称前添加下划线
#demo.py
def say():
print("人生苦短,我学Python!")
def CLanguage():
print("C语言中文网:http://c.biancheng.net")
def _disPython(): # 加下划线
print("Python教程:http://c.biancheng.net/python")
#test.py
from demo import * # 这种写法还可以通过在模块中定义__all__变量,来指定模块中的成员导入,比如demo.py中加上 __all__ = ["say","CLanguage"], 下面disPython也会报错,当然这个仅限于from demo import *的写法
say()
CLanguage()
disPython() # NameError: name 'disPython' is not defined
自定义模块: 我们也可以在写代码的时候,把一些公用的功能函数,比如处理时间的, 处理日志的,处理定时任务的等定义成模块,供其他程序调用,这个也比较简单
python
# 写个demo.py
class CLanguage:
def __init__(self,name,add):
self.name = name
self.add = add
def say(self):
print(self.name,self.add)
# 为了检验模板中代码的正确性,我们往往需要为其设计一段测试代码, 但注意测试代码要写到if __name__ == "__main__"这里面
# 假设去掉这个东西, 那么后面在其他程序里面导入这个包,都会先显示测试代码
# 我之前都是测试完了这个包,然后把下面的测试代码注释掉,原来不用注释掉也无所谓, 只要有main这行代码, 保证的效果就是
# 只有直接运行模板文件时,测试代码才会被执行;反之,如果是其它程序以引入的方式执行模板文件,则测试代码不应该被执行
# 原理:
# Python 内置的 __name__ 变量。当直接运行一个模块时,name 变量的值为 __main__,
# 而将模块被导入其他程序中并运行该程序时,处于模块中的 __name__ 变量的值就变成了模块名。
# 因此,如果希望测试函数只有在直接运行模块文件时才执行,则可在调用测试函数时增加判断,即只有当 __name__ =='__main__' 时才调用测试函数。
if __name__ == "__main__": # 作用是确保只有单独运行该模块时,此表达式才成立,才可以进入此判断语法,执行其中的测试代码;反之,如果只是作为模块导入到其他程序文件中,则此表达式将不成立,运行其它程序时,也就不会执行该判断语句中的测试代码
c = CLanguage("zhongqiang", "hello")
c.say()
# 写个test.py
import demo
c = demo.CLanguage("zhongqiang", "hello")
c.say() # zhongqiang hello
# 自定义函数还可以编写文档说明
# 为自定义模块添加说明文档,和函数或类的添加方法相同,即只需在模块开头的位置定义一个字符串即可
# 比如为demo.py编写一段
'''
demo 模块中包含以下内容:
CLanguage类:包含 name 和 add 属性和 say() 方法。
'''
# 这样在test.py里面,可以
print(demo.__doc__) # 打印demo的文档描述
# ModuleNotFoundError: No module named '模块名' 问题
# Python 解释器查找模块文件的过程,通常情况下,当使用 import 语句导入模块后,Python 会按照以下顺序查找指定的模块文件:
# 在当前目录,即当前执行的程序文件所在目录下查找;
# 到 PYTHONPATH(环境变量)下的每个目录中查找;
# 到 Python 默认的安装目录下查找。
# 以上所有涉及到的目录,都保存在标准模块 sys 的 sys.path 变量中,通过此变量我们可以看到指定程序文件支持查找的所有目录。
# 换句话说,如果要导入的模块没有存储在 sys.path 显示的目录中,那么导入该模块并运行程序时,Python 解释器就会抛出 ModuleNotFoundError(未找到模块)异常
# 解决方法:
# 1. 向 sys.path 中临时添加模块文件存储位置的完整路径;
import sys
sys.path.append('D:\\python_module')
# 2. 将模块放在 sys.path 变量中已包含的模块加载路径中;
# 直接将我们已编写好的 xxx.py 文件添加到 python安装目录\lib\site-packages 路径下,具体可以打印sys.path的值看下
# 3. 设置 path 系统环境变量
# PYTHONPATH 环境变量(简称 path 变量)的值是很多路径组成的集合,Python 解释器会按照 path 包含的路径进行一次搜索,直到找到指定要加载的模块。当然,如果最终依旧没有找到,则 Python 就报 ModuleNotFoundError 异常
#设置PYTHON PATH 环境变量
# Linux 平台的环境变量是通过 .bash_profile 文件来设置的,使用无格式编辑器打开该文件,在该文件中添加 PYTHONPATH 环境变量
export PYTHONPATH=.:/home/mengma/python_module
source .bash_profile
python包:实际开发中,一个大型的项目往往需要使用成百上千的 Python 模块,如果将这些模块都堆放在一起,势必不好管理。而且,使用模块可以有效避免变量名或函数名重名引发的冲突,但是如果模块名重复怎么办呢?因此,Python提出了包(Package)的概念。
简单理解,包就是文件夹,只不过在该文件夹下会存在一个名为"init.py" 的文件。
每个包的目录下都必须建立一个 init.py 的模块,可以是一个空模块,可以写一些初始化代码,其作用就是告诉 Python 要将该目录当成包来处理
注意,init .py 不同于其他模块文件,此模块的模块名不是 init ,而是它所在的包名。例如,在 settings 包中的 init.py 文件,其模块名就是 settings。
包是一个包含多个模块的文件夹,它的本质依然是模块.
init.py文件的功效:
导入包就等同于导入该包中的 init .py 文件,因此完全可以在 init .py 文件中直接编写实现模块功能的变量、函数和类,但实际上并推荐大家这样做,因为包的主要作用是包含多个模块。因此 init.py 文件的主要作用是导入该包内的其他模块
python
# 创建包, 创建个目录,初始化一个__init__.py文件, 然后就可以放整成的模块文件
my_package
┠── __init__.py
┠── module1.py
┗━━ module2.py
# 导入方法
# 注意,导入包的同时,会在包目录下生成一个含有 __init__.cpython-36.pyc 文件的 __pycache__ 文件夹。
# import 包名[.模块名 [as 别名]]
# from 包名 import 模块名 [as 别名]
# from 包名.模块名 import 成员名 [as 别名]
import my_package.module1 as xxx
from my_package import module1 as xxx # 使用此语法格式导入包中模块后,在使用其成员时不需要带包名前缀,但需要带模块名前缀
from my_package.module1 import display # 可以直接使用类和变量
# 通过在 __init__.py 文件使用 import 语句将必要的模块导入
# 这样当向其他程序中导入此包时,就可以直接导入包名,也就是使用import 包名(或from 包名 import *)的形式即可
# __init__.py文件中
# 从当前包导入 module1 模块
from . import module1
#from .module1 import *
# 从当前包导入 module2 模块
#from . import module2
from .module2 import *
# 第一种方式使用
# 用于导入当前包(模块)中的指定模块,这样即可在包中使用该模块。当在其他程序使用模块内的成员时,需要添加"包名.模块名"作为前缀
import my_package
my_package.module1.xxx函数
# 第二种方式种用
# 表示从指定模块中导入所有成员,采用这种导入方式,在其他程序中使用该模块的成员时,只要使用包名作为前缀即可
import my_package
clangs = my_package.CLanguage()
正确导入模块或者包之后,怎么知道该模块中具体包含哪些成员(变量、函数或者类)呢?
-
dir()函数: 查看某指定模块包含的全部成员(包括变量、函数和类)。注意这里所指的全部成员,不仅包含可供我们调用的模块成员,还包含所有名称以双下划线"__"开头和结尾的成员,而这些"特殊"命名的成员,是为了在本模块中使用的,并不希望被其它文件调用。
pythonimport string print(dir(string)) # 忽略显示 dir() 函数输出的特殊成员的方法,这些特殊方法对我们没有意义 print([e for e in dir(string) if not e.startswith('_')])
-
all变量:借助该变量也可以查看模块(包)内包含的所有成员
pythonimport string print(string.__all__) # ['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template'] # __all__ 变量在查看指定模块成员时,它不会显示模块中的特殊成员,同时还会根据成员的名称进行排序显示。 # 需要注意的是,并非所有的模块都支持使用 __all__ 变量,因此对于获取有些模块的成员,就只能使用 dir() 函数
-
file属性: 可以查看模块的源文件路径
pythonimport string print(string.__file__) # xxx:\python3.6/lib/string.py # 通过__file__属性输出的绝对路径, 可以很轻松找到该模块或包的源文件
下面整理好用的python包, 包括内建和第三方库。
3. python常用包大总结
3.1 枚举类(enum包)
一些具有特殊含义的类,其实例化对象的个数往往是固定的,比如月份,周等
对于这些实例化对象个数固定的类,可以用枚举类来定义。
python
from enum import Enum, auto
# Enum 创建枚举型常数的基类 成员值类型可以是int, str等,任意类型
# IntEnum 用于创建同时也是 int 的子类的枚举型常数的基类, 成员值类型是int
class Color(Enum):
# 为序列值指定value值
red = 1
green = 2
blue = 3
# 除了通过继承 Enum 类的方法创建枚举类,还可以使用 Enum() 函数创建枚举类
Color = Enum("Color",('red','green','blue'))
# 枚举类不能用来实例化对象 访问成员用下面的方法
Color.red.value # 1
Color.red.name # red
for color in Color:
print(color.name, color.value)
# 注意,枚举类的每个成员都由 2 部分组成,分别为 name 和 value,其中 name 属性值为该枚举值的变量名(如 red),value 代表该枚举值的序号(序号通常从 1 开始)
# 枚举类成员之间不能比较大小,但可以用 == 或者 is 进行比较是否相等 Color.red == Color.green
# 枚举类中各个成员的值,不能在类的外部做任何修改 Color.red=4 error
# 该枚举类还提供了一个 __members__ 属性,该属性是一个包含枚举类中所有成员的字典,通过遍历该属性,也可以访问枚举类中的各个成员
for name,member in Color.__members__.items():
print(name,"->",member)
# Python 枚举类中各个成员必须保证 name 互不相同,但 value 可以相同
class Color(Enum):
# 为序列值指定value值
red = 1
green = 1
blue = 3
print(Color['green']) # red red 和 green 具有相同的值(都是 1),Python 允许这种情况的发生,它会将 green 当做是 red 的别名,因此当访问 green 成员时,最终输出的是 red
# 如果想避免值相同的情况, 需要借助@unique装饰器
#引入 unique
from enum import Enum,unique
#添加 unique 装饰器
@unique
class Color(Enum):
# 为序列值指定value值
red = 1
green = 1
blue = 3
print(Color['green']) # ValueError: duplicate values found in <enum 'Color'>: green -> red
# 可以使用自动设定的值
class Color(Enum):
red = auto()
green = auto()
blue = auto()
3.2 类型提示和注解(typing包)
Python是一门动态语言,很多时候我们可能不清楚函数参数类型或者返回值类型,很有可能导致一些类型没有指定方法,在写完代码一段时间后回过头看代码,很可能忘记了自己写的函数需要传什么参数,返回什么类型的结果。
Python的typing包是从Python 3.5版本引入的标准库,它提供了类型提示和类型注解的功能 ,用于对代码进行静态类型检查和类型推断。typing模块中定义了多种类型和泛型,以帮助开发者代码的可读性、可维护性和可靠性。
主要功能:
- 类型注解: typing包提供了多种用于类型注解的工具,包括基本类型(如int、str)、容器类型(如List、Dict)、函数类型(如Callable、Tuple)、泛型(如Generic、TypeVar)等。通过类型注解,可以在函数声明、变量声明和类声明中指定参数的类型、返回值的类型等,以增加代码的可读性和可靠性。
- 类型检查:通过与第三方工具(如mypy)集成,可以对使用了类型注解的代码进行静态类型检查。类型检查可以帮助发现潜在的类型错误和逻辑错误,以提前捕获问题并改善代码的质量。
- 泛型支持:typing模块提供了对泛型的支持,使得可以编写更通用和灵活的代码。通过泛型,可以在函数和类中引入类型参数,以处理各种类型的数据。
- 类、函数和变量装饰器:typing模块提供了一些装饰器,如@overload、@abstractmethod、@final等,用于修饰类、函数和变量,以增加代码的可读性和可靠性。
官方文档 https://docs.python.org/zh-cn/3/library/typing.html#
下面整理常用的:
python
# typing中的基本类型
# int: 整数类型
# float: 浮点数类型
# bool: 布尔类型
# str: 字符串类型
# bytes: 字节类型
# Any: 任意类型
# Union: 多个类型的联合类型,表示可以是其中任意一个类型
# Tuple: 固定长度的元组类型
# List: 列表类型
# Dict: 字典类型,用于键值对的映射
# typing中的泛型
# Generic: 泛型基类,用于创建泛型类或泛型函数
# TypeVar: 类型变量,用于创建表示不确定类型的占位符
# Callable: 可调用对象类型,用于表示函数类型
# Optional: 可选类型,表示一个值可以为指定类型或None
# Iterable: 可迭代对象类型
# Mapping: 映射类型,用于表示键值对的映射
# Sequence: 序列类型,用于表示有序集合类型
# Type:泛型类,用于表示类型本身
from typing import List, Tuple, Union, Dict, Mapping, ...
# List, typing中的List可以帮助我们知道列表里面的元素是什么样子的
var: List[int or float] = [2, 3.5]
var: List[List[int]] = [[1, 2], [2, 3]]
# Tuple
person: Tuple[str, int, float] = ('Mike', 22, 1.75)
# Dict, Mapping
# Dict、字典,是 dict 的泛型;Mapping,映射,是 collections.abc.Mapping 的泛型
# 据官方文档,Dict 推荐用于注解返回类型,Mapping 推荐用于注解参数。它们的使用方法都是一样的,其后跟一个中括号,中括号内分别声明键名、键值的类型
def size(rect: Mapping[str, int]) -> Dict[str, int]:
return {'width': rect['width'] + 100, 'height': rect['width'] + 100}
# Set, AbstractSet
# Set、集合,是 set 的泛型;AbstractSet、是 collections.abc.Set 的泛型。
# 根据官方文档,Set 推荐用于注解返回类型,AbstractSet 用于注解参数
# 使用方法都是一样的,其后跟一个中括号,里面声明集合中元素的类型,如:
def describe(s: AbstractSet[int]) -> Set[int]:
return set(s)
# Any 一种特殊的类型,它可以代表所有类型,静态类型检查器的所有类型都与 Any 类型兼容,所有的无参数类型注解和返回类型注解的都会默认使用 Any 类型
def add(a: Any) -> Any:
return a + 1
# Sequence
# collections.abc.Sequence 的泛型,在某些情况下,我们可能并不需要严格区分一个变量或参数到底是列表 list 类型还是元组 tuple 类型
# 我们可以使用一个更为泛化的类型,叫做 Sequence,其用法类似于 List
def square(elements: Sequence[float]) -> List[float]:
return [x ** 2 for x in elements]
# NoReturn
# NoReturn,当一个方法没有返回结果时,为了注解它的返回类型,我们可以将其注解为 NoReturn
def hello() -> NoReturn:
print('hello')
# TypeVar
# 可以借助它来自定义兼容特定类型的变量,比如有的变量声明为 int、float、None 都是符合要求的
# 实际就是代表任意的数字或者空内容都可以,其他的类型则不可以,比如列表 list、字典 dict 等等,像这样的情况,我们可以使用 TypeVar 来表示。
# 例如一个人的身高,便可以使用 int 或 float 或 None 来表示,但不能用 dict 来表示
height = 1.75
Height = TypeVar('Height', int, float, None)
def get_height() -> Height:
return height
# NewType
# NewType,我们可以借助于它来声明一些具有特殊含义的类型,例如像 Tuple 的例子一样,我们需要将它表示为 Person,即一个人的含义,但从表面上声明为 Tuple 并不直观
# 所以我们可以使用 NewType 为其声明一个类型
Person = typing.NewType('Person', typing.Tuple[str, int, float])
person = Person(('Mike', 22, 1.75))
print(person[0]) # person就和tuple一样
print(isinstance(person, tuple)) # True
# Callable 可调用类型
# 通常用来注解一个方法
def date(year: int, month: int, day: int) -> str:
return f'{year}-{month}-{day}'
def get_date_fn() -> Callable[[int, int, int], str]:
return date
# Union 联合类型 Union[X, Y] 代表要么是 X 类型,要么是 Y 类型 Union[int, str, float]
def process(fn: Union[str, Callable]):
isinstance(fn, str):
# str2fn and process
pass
isinstance(, Callable):
fn()
# Optional
# 这个参数可以为空或已经声明的类型,即 Optional[X] 等价于 Union[X, None]。
# 但值得注意的是,这个并不等价于可选参数,当它作为参数类型注解的时候,不代表这个参数可以不传递了,而是说这个参数可以传为 None
def judge(result: bool) -> Optional[str]:
if result: return 'Error Occurred'
# Generator
# 如果想代表一个生成器类型,可以使用 Generator,它的声明比较特殊
# 其后的中括号紧跟着三个参数,分别代表 YieldType、SendType、ReturnType
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return 'Done'
# 类型别名, 可以简化一些复杂的签名
from collections.abc import Sequence
type ConnectionOptions = dict[str, str]
type Address = tuple[str, int]
type Server = tuple[Address, ConnectionOptions]
def broadcast_message(message: str, servers: Sequence[Server]) -> None:
...
# 当一个函数的参数与返回都加上注解, help(函数)的时候也能看到,增加了代码的可读性
类型注解,一些 IDE 是可以识别出来并提示的,比如 PyCharm 就可以识别出来在调用某个方法的时候参数类型不一致,会提示 WARNING,这样就可以帮助我们更规范的写代码。
3.3 集合类(Collections包)
待补充
3.4 日志类(logging包)
实用的记录日志库,存储各种格式的日志, 大型项目必备。print这种操作,小大小闹可以,但大项目里面一般没办法直接看看输出了啥东西,都是通过日志去排查问题。
logging的日志框架:
-
Loggers
: 可供程序直接调用的接口,可以直接向logger写入日志信息,app通过调用提供的api来记录日志 -
Handlers
: 将logger发过来的信息进行准确地分配,送往正确的地方。举个栗子,送往控制台或者文件或者both或者其他地方(进程管道之类的)。它决定了每个日志的行为,是之后需要配置的重点区域StreamHandler
标准流处理器,将消息发送到标准输出流、错误流FileHandler
文件处理器,将消息发送到文件RotatingFileHandler
文件处理器,文件达到指定大小后,启用新文件存储日志TimedRotatingFileHandler
文件处理器,日志以特定的时间间隔轮换日志文件
-
Filters
:提供了更细粒度的判断,来决定日志是否需要打印。原则上handler获得一个日志就必定会根据级别被统一处理,但是如果handler拥有一个Filter可以对日志进行额外的处理和判断。例如Filter能够对来自特定源的日志进行拦截or修改甚至修改其日志级别(修改后再进行级别判断) -
Formatters
: 制定最终记录打印的格式布局,指定了最终某条记录打印的格式布局。Formatter会将传递来的信息拼接成一条具体的字符串,默认情况下Format只会将信息%(message)s直接打印出来属性 格式 描述 asctime %(asctime)s 将日志时间构造成可读形式,默认是xxxx-xx-xx xx:xx:xx.xxx 精确到毫秒 filename %(filename)s 包含path的文件名 funcName %(funcName)s 哪个function发出的Bug levelname %(levelname)s 日志的最终等级(filter修改后的) message %(message)s 日志信息 lineno %(lineno)s 当前日志的行号 pathname %(pathname)s 完整路径 process %(process)s 当前进程 thread %(thread)s 当前线程
logging日志级别: 级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG
debug
: 打印全部的日志,详细的信息,通常只出现在诊断问题上, 一般应用调试过程,如每个算法循环的中间状态info
: 打印info,warning,error,critical级别的日志,确认一切按预期运行。 处理请求或状态变化等日常信息warning
: 打印warning,error,critical级别的日志,一个迹象表明,一些意想不到的事情发生了,或表明一些问题在不久的将来(例如。磁盘空间低"),这个软件还能按预期工作。 发生很重要的事件,但并不是错误,如用户登陆密码错误error
: 打印error,critical级别的日志,更严重的问题,软件没能执行一些功能, 发生错误,如IO操作失败或连接问题critical
: 打印critical级别,一个严重的错误,这表明程序本身可能无法继续运行, 特别严重问题,如内存耗尽, 磁盘空间满等,很少使用
python
# 基本使用, 简单将日志打印到屏幕
import logging
logging.debug('this is debug message')
logging.info('this is info message')
logging.warning('this is warning message') # 默认只会打印这个,原因默认情况下,logging将日志打印到屏幕,日志级别为WARNING, 所以会把WARING及以上的日志打印。
# 通过logging.basicConfig函数对日志的输出格式及方式做相关配置
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(message)s') # 这样会把上面的3条信息都打印出来 2024-05-14 12:07:17,030 - root - this is debug message
# 参数说明
# filename: 指定日志文件名
# filemode: 和file函数意义相同,指定日志文件的打开模式,'w'或'a'
# format: 指定输出的格式和内容,format可以输出很多有用信息,如上例所示:
# %(levelno)s: 打印日志级别的数值
# %(levelname)s: 打印日志级别名称 (这个有用)
# %(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
# %(filename)s: 打印当前执行程序名 (这个有用)
# %(funcName)s: 打印日志的当前函数 (这个有用)
# %(lineno)d: 打印日志的当前行号 (这个有用)
# %(asctime)s: 打印日志的时间 (这个有用)
# %(thread)d: 打印线程ID
# %(threadName)s: 打印线程名称
# %(process)d: 打印进程ID
# %(message)s: 打印日志信息 (这个有用)
# datefmt: 指定时间格式,同time.strftime()
# level: 设置日志级别,默认为logging.WARNING, 这个level是最低日志级别
# stream: 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略
# 这里记录一个我自己调教的格式 级别,时间,文件,函数,行,信息都带着,排查问题一目了然
log_format = "%(levelname)s: %(asctime)s %(filename)s[line:%(lineno)d,func:%(funcName)s] %(message)s"
logging.basicConfig(level=logging.DEBUG, format=log_format)
# 日志输出到文件
logging.basicConfig(filename="test.log", filemode='w', level=logging.INFO, format=log_format)
# 每次重新运行时,日志会以追加的方式在后面, 如果每次运行前要覆盖之前的日志,则需指定 filemode='w', 这个和 open 函数写数据到文件用的参数是一样的
进阶使用:利用logging库提供的组件
前面介绍的日志记录,其实都是通过一个叫做日志记录器(Logger)的实例对象创建的,每个记录器都有一个名称,直接使用logging来记录日志时,系统会默认创建 名为 root 的记录器,这个记录器是根记录器。记录器支持层级结构,子记录器通常不需要单独设置日志级别以及Handler(后面会介绍),如果子记录器没有单独设置,则它的行为会委托给父级
python
# 记录器logger
import logging
# 设置基本配置
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.info("xxx")
logger.error("xxx")
logger.debug("xxx")
# 处理器 Handler
# 记录器负责日志的记录,但是日志最终记录在哪里记录器并不关心,而是交给了另一个家伙--处理器(Handler)去处理
# 例如一个Flask项目,你可能会将INFO级别的日志记录到文件,将ERROR级别的日志记录到标准输出,将某些关键日志(例如有订单或者严重错误)发送到某个邮件地址通知老板。
# 这时候你的记录器添加多个不同的处理器来处理不同的消息日志,以此根据消息的重要性发送的特定的位置
# Handler 提供了4个方法给开发者使用,logger可以设置level,Handler也可以设置Level。通过setLevel可以将记录器记录的不同级别的消息发送到不同的地方去
from logging import StreamHandler
from logging import FileHandler
logger = logging.getLogger(__name__)
# 虽然不是非得将 logger 的名称设置为 __name__ ,但是这样做会给我们带来诸多益处。
# 在 python 中,变量 __name__ 的名称就是当前模块的名称。
# 比如,在模块 "foo.bar.my_module" 中调用 logger.getLogger(__name__) 等价于调用logger.getLogger("foo.bar.my_module") 。
# 当你需要配置 logger 时,你可以配置到 "foo" 中,这样包 foo 中的所有模块都会使用相同的配置。当你在读日志文件的时候,你就能够明白消息到底来自于哪一个模块。
# 设置为DEBUG级别
logger.setLevel(logging.DEBUG)
# 标准流处理器,设置的级别为WARAING
stream_handler = StreamHandler()
stream_handler.setLevel(logging.WARNING)
logger.addHandler(stream_handler)
# 文件处理器,设置的级别为INFO
file_handler = FileHandler(filename="test.log")
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
logger.debug("this is debug")
logger.info("this is info")
logger.error("this is error")
logger.warning("this is warning")
# 此时, 命令行输出内容是warning及以上级别的内容, 输出到文件的是info及以上大内容
# 尽管我们将logger的级别设置为了DEBUG,但是debug记录的消息并没有输出,因为我给两个Handler设置的级别都比DEBUG要高,所以这条消息被过滤掉了
# 格式器 formatter
# 格式器不仅可以通过logging.basicConfig来指定,还可以以对象的形式设置在Handler上。
# 标准流处理器
stream_handler = StreamHandler()
stream_handler.setLevel(logging.WARNING)
# 创建一个格式器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# 作用在handler上
stream_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(stream_handler)
logger.info("this is info")
logger.error("this is error")
logger.warning("this is warning")
# 注意,格式器只能作用在处理器上,通过处理器的setFromatter方法设置格式器。而且一个Handler只能设置一个格式器。是一对一的关系。
# 而 logger 与 handler 是一对多的关系,一个logger可以添加多个handler。 handler 和 logger 都可以设置日志的等级
逻辑如下:
logging.basicConfig
背后做的事情:
- 创建一个root记录器
- 设置root的日志级别为warning
- 为root记录器添加StreamHandler处理器
- 为处理器设置一个简单格式器
python
import logging
logging.basicConfig()
logging.warning("hello")
# 上面代码等价于
from logging import StreamHandler
from logging import Formatter
logger = logging.getLogger("root")
logger.setLevel(logging.WARNING)
handler = StreamHandler(sys.stderr)
logger.addHandler(handler)
formatter = Formatter(" %(levelname)s:%(name)s:%(message)s")
handler.setFormatter(formatter)
logger.warning("hello")
logging.basicConfig
方法做的事情是相当于给日志系统做一个最基本的配置,方便开发者快速接入使用。它必须在开始记录日志前调用。不过如果 root 记录器已经指定有其它处理器,这时候你再调用basciConfig,则该方式将失效,它什么都不做
日志回滚:
如果你用 FileHandler 写日志,文件的大小会随着时间推移而不断增大。最终有一天它会占满你所有的磁盘空间。为了避免这种情况出现,你可以在你的生成环境中使用 RotatingFileHandler 替代 FileHandler
python
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger(__name__)
logger.setLevel(level = logging.INFO)
# 定义一个RotatingFileHandler,最多备份3个日志文件,每个日志文件最大1K
rHandler = RotatingFileHandler("log.txt",maxBytes = 1*1024,backupCount = 3)
rHandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
rHandler.setFormatter(formatter)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(formatter)
logger.addHandler(rHandler)
logger.addHandler(console)
logger.info("Start print log")
logger.debug("Do something")
logger.warning("Something maybe fail.")
logger.info("Finish")
日志配置:日志的配置除了前面介绍的将配置直接写在代码中,还可以将配置信息单独放在配置文件中,实现配置与代码分离。
日志配置文件:
python
[loggers]
keys=root
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
加载配置:
python
import logging
import logging.config
# 加载配置
logging.config.fileConfig('logging.conf')
# 创建 logger
logger = logging.getLogger()
# 应用代码
logger.debug("debug message")
logger.info("info message")
logger.warning("warning message")
logger.error("error message")
这个conf的不太好理解,下面整理一个日志配置字典的方式,这个比较好理解。
下面结合我实践写一个比较规范的代码。
-
建立一个log模块(可以是在自己的大型项目下)
-
里面写一个
__init__.py
函数pythonfrom . import log
-
写一个
log_conf.json
文件,这是我调教的一个, 生成的日志会根据等级不同用不同的颜色, 先安个colorlog
包(pip install
)python{ # 使用的python内置的logging模块,那么python可能会对它进行升级,所以需要写一个版本号,目前就是1版本 "version":1, # 是否去掉目前项目中其他地方中以及使用的日志功能,将来我们可能会引入第三方的模块,里面可能内置了日志功能,肯定是不影响其他日志功能,所以这里要设置成False "disable_existing_loggers":false, # 日志的处理格式 "formatters":{ # 详细格式,往往用于记录日志到文件/其他第三方存储设备 "verbose": { "format": "%(levelname)s: %(asctime)s %(filename)s[line:%(lineno)d,module:%(name)s] %(message)s" }, # 带颜色的格式, 一般用于控制台输出 "color_verbose": { "()": "colorlog.ColoredFormatter", "format": "%(log_color)s%(levelname)s: %(asctime)s %(filename)s[line:%(lineno)d,module:%(name)s] %(message)s", # 设置不同等级的颜色 "log_colors": { "DEBUG": "cyan", "INFO": "green", "WARNING": "yellow", "ERROR": "red", "CRITICAL": "red" } } }, #'filters': { # 日志的过滤设置,可以对日志进行输出时的过滤用的 # 在debug=True下产生的一些日志信息,要不要记录日志,需要的话就在handlers中加上这个过滤器,不需要就不加 # 'require_debug_true': { # '()': Filter, # }, # }, # 日志的处理方式 "handlers":{ # 终端下展示 "console":{ "class":"logging.StreamHandler", "level":"DEBUG", "formatter":"color_verbose", "stream":"ext://sys.stdout" }, # 输出到文件 info层级 "info_file_handler":{ "class":"logging.handlers.RotatingFileHandler", "level":"INFO", # 日志层级 "formatter":"verbose", # 使用的格式 "filename":"info.log", # 日志位置,日志文件名,日志保存目录必须手动创建 "maxBytes":10485760, # 最大字节数 "backupCount":20, # 备份日志文件的数量,设置最大日志数量为10 "encoding":"utf8" # 设置默认编码,否则打印出来汉字乱码 }, # 输出到文件 error层级 "error_file_handler":{ "class":"logging.handlers.RotatingFileHandler", "level":"ERROR", "formatter":"verbose", "filename":"errors.log", "maxBytes":10485760, "backupCount":20, "encoding":"utf8" } }, # 日志实例对象, 可以通过logging.getLogger("my_module")获取这里的实例对象 "loggers":{ "my_module":{ "level":"ERROR", "handlers":["info_file_handler"], "propagate":"no" # 是否让日志信息继续冒泡给其他的日志处理系统 } }, # 根目录 可以通过logging.getLogger(__name__)搞这个通用的, 这个和上面的区别是,假设有好几个模块, 好几个模块可以用这个通用的处理,也可以在上面loggers根据模块名使得不同模块使用不同的设置,非常灵活 "root":{ "level":"INFO", "handlers":["console","info_file_handler","error_file_handler"] } }
-
写一个
log.py
文件pythonimport json import logging import logging.config import os.path def get_logger(name, log_dir: str = "/home/work/logs/artifact_logs", conf_file: str = "/home/work/trigger/web/log/log_conf.json"): if not os.path.exists(log_dir): os.mkdir(log_dir) if os.path.exists(conf_file): with open(conf_file) as f: logging_conf = json.load(f) # 修改日志文件存储路径 logging_conf['handlers']['info_file_handler']['filename'] = os.path.join(log_dir, "info.log") logging_conf['handlers']['error_file_handler']['filename'] = os.path.join(log_dir, "errors.log") logging.config.dictConfig(logging_conf) else: log_format = "%(levelname)s: %(asctime)s %(filename)s[line:%(lineno)d,module:%(name)s] %(message)s" logging.basicConfig(level=logging.INFO, format=log_format) logger = logging.getLogger(name) return logger if __name__ == "__main__": logger = get_logger(name=__name__, log_dir='./', conf_file='./log_conf.json') logger.info("日志测试") logger.warning("程序警告") logger.error("程序出问题了")
-
这样,如果再项目的其他文件中用到,直接导入使用即可
python# 比如我在log模块同级目录下有个biz目录,里面有个artifact_biz.py文件,文件开头加一个 from web.log import log logger = get_logger(__name__) # 后面统一用这个logger打印日志即可,也可以自定义日志文件输出的log_dir
效果如下:这样再从文件查看日志,就一目了然了。
-
捕捉异常并使用traceback记录
pythona = {"hello": "world"} try: print(a["name"]) except Exception as e: logger.error("Error", exc_info=True) # 也可以调用 logger.exception(msg, _args),它等价于 logger.error(msg, exc_info=True, _args)。 # 这个会直接出来Traceback的信息 ERROR: 2024-05-15 14:17:31,409 log.py[line:55,module:__main__] Error Traceback (most recent call last): File "/home/wuzhongqiang/PycharmHome/ad-cloud/search/trigger/web/log/log.py", line 53, in <module> print(a["name"]) KeyError: 'name'
更详细的使用方法, 查看这篇文章https://www.cnblogs.com/deeper/p/7404190.html
2.3.5 functools包
待补充
4. 小总
这篇文章主要想从实用的角度去整理一些常用的包,比如自定义一个日志模块, 使用typing做注解,包的一些导入细节等,使得后面写代码时能更加高效,可读,可维护,方便调试和监控, 关于知识的学习,发现从实践的角度更能加深内容的理解和思考,和之前学习python的感觉不太一样, 知识学以致用,才是自己的, 加油呀 😉
参考