python实践笔记(一): 模块和包

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()

正确导入模块或者包之后,怎么知道该模块中具体包含哪些成员(变量、函数或者类)呢?

  1. dir()函数: 查看某指定模块包含的全部成员(包括变量、函数和类)。注意这里所指的全部成员,不仅包含可供我们调用的模块成员,还包含所有名称以双下划线"__"开头和结尾的成员,而这些"特殊"命名的成员,是为了在本模块中使用的,并不希望被其它文件调用。

    python 复制代码
    import string
    print(dir(string))
    # 忽略显示 dir() 函数输出的特殊成员的方法,这些特殊方法对我们没有意义
    print([e for e in dir(string) if not e.startswith('_')]) 
  2. all变量:借助该变量也可以查看模块(包)内包含的所有成员

    python 复制代码
    import string
    print(string.__all__)  # ['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template']
    
    # __all__ 变量在查看指定模块成员时,它不会显示模块中的特殊成员,同时还会根据成员的名称进行排序显示。
    # 需要注意的是,并非所有的模块都支持使用 __all__ 变量,因此对于获取有些模块的成员,就只能使用 dir() 函数
  3. file属性: 可以查看模块的源文件路径

    python 复制代码
    import 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模块中定义了多种类型和泛型,以帮助开发者代码的可读性、可维护性和可靠性。

主要功能:

  1. 类型注解: typing包提供了多种用于类型注解的工具,包括基本类型(如int、str)、容器类型(如List、Dict)、函数类型(如Callable、Tuple)、泛型(如Generic、TypeVar)等。通过类型注解,可以在函数声明、变量声明和类声明中指定参数的类型、返回值的类型等,以增加代码的可读性和可靠性。
  2. 类型检查:通过与第三方工具(如mypy)集成,可以对使用了类型注解的代码进行静态类型检查。类型检查可以帮助发现潜在的类型错误和逻辑错误,以提前捕获问题并改善代码的质量。
  3. 泛型支持:typing模块提供了对泛型的支持,使得可以编写更通用和灵活的代码。通过泛型,可以在函数和类中引入类型参数,以处理各种类型的数据。
  4. 类、函数和变量装饰器: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的日志框架:

  1. Loggers: 可供程序直接调用的接口,可以直接向logger写入日志信息,app通过调用提供的api来记录日志

  2. Handlers: 将logger发过来的信息进行准确地分配,送往正确的地方。举个栗子,送往控制台或者文件或者both或者其他地方(进程管道之类的)。它决定了每个日志的行为,是之后需要配置的重点区域

    1. StreamHandler 标准流处理器,将消息发送到标准输出流、错误流
    2. FileHandler 文件处理器,将消息发送到文件
    3. RotatingFileHandler 文件处理器,文件达到指定大小后,启用新文件存储日志
    4. TimedRotatingFileHandler 文件处理器,日志以特定的时间间隔轮换日志文件
  3. Filters:提供了更细粒度的判断,来决定日志是否需要打印。原则上handler获得一个日志就必定会根据级别被统一处理,但是如果handler拥有一个Filter可以对日志进行额外的处理和判断。例如Filter能够对来自特定源的日志进行拦截or修改甚至修改其日志级别(修改后再进行级别判断)

  4. 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

  1. debug : 打印全部的日志,详细的信息,通常只出现在诊断问题上, 一般应用调试过程,如每个算法循环的中间状态
  2. info : 打印info,warning,error,critical级别的日志,确认一切按预期运行。 处理请求或状态变化等日常信息
  3. warning : 打印warning,error,critical级别的日志,一个迹象表明,一些意想不到的事情发生了,或表明一些问题在不久的将来(例如。磁盘空间低"),这个软件还能按预期工作。 发生很重要的事件,但并不是错误,如用户登陆密码错误
  4. error : 打印error,critical级别的日志,更严重的问题,软件没能执行一些功能, 发生错误,如IO操作失败或连接问题
  5. 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 背后做的事情:

  1. 创建一个root记录器
  2. 设置root的日志级别为warning
  3. 为root记录器添加StreamHandler处理器
  4. 为处理器设置一个简单格式器
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的不太好理解,下面整理一个日志配置字典的方式,这个比较好理解。

下面结合我实践写一个比较规范的代码。

  1. 建立一个log模块(可以是在自己的大型项目下)

  2. 里面写一个__init__.py函数

    python 复制代码
    from . import log
  3. 写一个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"]
        }
    }
  4. 写一个log.py文件

    python 复制代码
    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("程序出问题了")
  5. 这样,如果再项目的其他文件中用到,直接导入使用即可

    python 复制代码
    # 比如我在log模块同级目录下有个biz目录,里面有个artifact_biz.py文件,文件开头加一个
    from web.log import log
    logger = get_logger(__name__)  # 后面统一用这个logger打印日志即可,也可以自定义日志文件输出的log_dir

    效果如下:这样再从文件查看日志,就一目了然了。

  6. 捕捉异常并使用traceback记录

    python 复制代码
    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

2.3.5 functools包

待补充

4. 小总

这篇文章主要想从实用的角度去整理一些常用的包,比如自定义一个日志模块, 使用typing做注解,包的一些导入细节等,使得后面写代码时能更加高效,可读,可维护,方便调试和监控, 关于知识的学习,发现从实践的角度更能加深内容的理解和思考,和之前学习python的感觉不太一样, 知识学以致用,才是自己的, 加油呀 😉

参考

相关推荐
程序小武1 小时前
python编辑器如何选择?
后端·python
一叶知秋12111 小时前
UV管理python项目
python
AndrewHZ1 小时前
【图像处理入门】2. Python中OpenCV与Matplotlib的图像操作指南
图像处理·python·opencv·计算机视觉·matplotlib·图像操作
golitter.2 小时前
langchain学习 01
python·学习·langchain
一叶知秋12112 小时前
LangChain Prompts模块
python
量化金策3 小时前
截面动量策略思路
python
心软且酷丶3 小时前
leetcode:7. 整数反转(python3解法,数学相关算法题)
python·算法·leetcode
逾非时3 小时前
python:selenium爬取网站信息
开发语言·python·selenium
天才测试猿3 小时前
Selenium操作指南(全)
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
不学无术の码农4 小时前
《Effective Python》第六章 推导式和生成器——避免在推导式中使用超过两个控制子表达式
开发语言·python