赞
踩
最近在重构之前的后端代码,借着这个机会又重新补充了关于python的一些知识, 学习到了一些高效编写代码的方法和心得,比如构建大项目来讲,要明确捕捉异常机制的重要性, 学会使用try...except..finally
, 要通过日志模块logging监控整个系统的性能,学会把日志输出到文件并了解日志层级,日志模块的一些工作原理,能自定义日志,这样能非常方便的debug服务,学会使用装饰器对关键接口进行时间耗时统计,日志打印等装饰,减少代码的重复性, 学会使用类的类方法,静态方法对一些公用函数进行封装,来增强代码的可维护性, 学会使用文档对函数和参数做注解, 学会函数的可变参数统一代码的风格等等, 这样能使得代码从可读性,可维护性, 灵活性和执行效率上都有一定的提升,写出来的代码也更加优美一些。 所以把这几天的学习,分模块整理几篇笔记, 这次是从实践的再去补充python的内容,目的是要写出漂亮的python代码,增强代码的可读,可维护,灵活和高效,方便调试和监控。
这一篇文章是模块和包, 这里面会整理python里面用到的比较高效的一些处理模块, 比如collections, loggings, enum, typing
等, 学会巧用这些模块,能让写出来的代码更加漂亮,后面再有新的模块也会补充。
大纲如下:
ok, let’s go!
模块,可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突。这样可以提高代码的可维护性和重用性。
# 模块导入的方式 # 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
自定义模块: 我们也可以在写代码的时候,把一些公用的功能函数,比如处理时间的, 处理日志的,处理定时任务的等定义成模块,供其他程序调用,这个也比较简单
# 写个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 文件的主要作用是导入该包内的其他模块
# 创建包, 创建个目录,初始化一个__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()函数: 查看某指定模块包含的全部成员(包括变量、函数和类)。注意这里所指的全部成员,不仅包含可供我们调用的模块成员,还包含所有名称以双下划线“__”开头和结尾的成员,而这些“特殊”命名的成员,是为了在本模块中使用的,并不希望被其它文件调用。
import string
print(dir(string))
# 忽略显示 dir() 函数输出的特殊成员的方法,这些特殊方法对我们没有意义
print([e for e in dir(string) if not e.startswith('_')])
all变量:借助该变量也可以查看模块(包)内包含的所有成员
import string
print(string.__all__) # ['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template']
# __all__ 变量在查看指定模块成员时,它不会显示模块中的特殊成员,同时还会根据成员的名称进行排序显示。
# 需要注意的是,并非所有的模块都支持使用 __all__ 变量,因此对于获取有些模块的成员,就只能使用 dir() 函数
file属性: 可以查看模块的源文件路径
import string
print(string.__file__) # xxx:\python3.6/lib/string.py
# 通过__file__属性输出的绝对路径, 可以很轻松找到该模块或包的源文件
下面整理好用的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()
Python是一门动态语言,很多时候我们可能不清楚函数参数类型或者返回值类型,很有可能导致一些类型没有指定方法,在写完代码一段时间后回过头看代码,很可能忘记了自己写的函数需要传什么参数,返回什么类型的结果。
Python的typing包是从Python 3.5版本引入的标准库,它提供了类型提示和类型注解的功能,用于对代码进行静态类型检查和类型推断。typing模块中定义了多种类型和泛型,以帮助开发者代码的可读性、可维护性和可靠性。
主要功能:
官方文档 https://docs.python.org/zh-cn/3/library/typing.html#
下面整理常用的:
# 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,这样就可以帮助我们更规范的写代码。
待补充
实用的记录日志库,存储各种格式的日志, 大型项目必备。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级别,一个严重的错误,这表明程序本身可能无法继续运行, 特别严重问题,如内存耗尽, 磁盘空间满等,很少使用# 基本使用, 简单将日志打印到屏幕 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(后面会介绍),如果子记录器没有单独设置,则它的行为会委托给父级
# 记录器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
背后做的事情:
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
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")
日志配置:日志的配置除了前面介绍的将配置直接写在代码中,还可以将配置信息单独放在配置文件中,实现配置与代码分离。
日志配置文件:
[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
加载配置:
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
函数
from . import log
写一个log_conf.json
文件,这是我调教的一个, 生成的日志会根据等级不同用不同的颜色, 先安个colorlog
包(pip install
)
{ # 使用的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
文件
import 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("程序出问题了")
这样,如果再项目的其他文件中用到,直接导入使用即可
# 比如我在log模块同级目录下有个biz目录,里面有个artifact_biz.py文件,文件开头加一个
from web.log import log
logger = get_logger(__name__) # 后面统一用这个logger打印日志即可,也可以自定义日志文件输出的log_dir
效果如下:这样再从文件查看日志,就一目了然了。
捕捉异常并使用traceback记录
a = {"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
待补充
这篇文章主要想从实用的角度去整理一些常用的包,比如自定义一个日志模块, 使用typing做注解,包的一些导入细节等,使得后面写代码时能更加高效,可读,可维护,方便调试和监控, 关于知识的学习,发现从实践的角度更能加深内容的理解和思考,和之前学习python的感觉不太一样, 知识学以致用,才是自己的, 加油呀 声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。