本文目录一览:
python装饰器有什么用
先来个形象比方 内裤可以用来遮羞,但是到了冬天它没法为我们防风御寒,聪明的人们发明了长裤,有了长裤后宝宝再也不冷了,装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。 再回到我们的主题 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。 先来看一个简单例子:
def foo():
print('i am foo')
现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:
def foo():
print('i am foo')
logging.info("foo is running")
bar()
、bar2()
也有类似的需求,怎么做?再写一个logging在bar
函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个函数:专门处理日志,日志处理完之后再执行真正的业务代码
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()
def bar():
print('i am bar')
use_logging(bar)
逻辑上不难理解,但是这样的话,我们每次都要将一个函数作为参数传递给use_logging
函数。而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,执行运行bar()
,但是现在不得不改成use_logging(bar)
。那么有没有更好的方式的呢?当然有,答案就是装饰器。
简单装饰器
def use_logging(func):
def wrapper(*args, **kwargs):
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
def bar():
print('i am bar')
bar = use_logging(bar)
bar()
函数use_logging
就是装饰器,它把执行真正业务方法的func
包裹在函数里面,看起来像bar
被use_logging
装饰了。在这个例子中,函数进入和退出时,被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。
@
符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作
def use_logging(func):
def wrapper(*args, **kwargs):
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
@use_logging
def foo():
print("i am foo")
@use_logging
def bar():
print("i am bar")
bar()
如上所示,这样我们就可以省去bar = use_logging(bar)
这一句了,直接调用bar()
即可得到想要的结果。如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。
装饰器在Python使用如此方便都要归因于Python的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
带参数的装饰器
装饰器还有更大的灵活性,例如带参数的装饰器:在上面的装饰器调用中,比如@use_logging
,该装饰器唯一的参数就是执行业务的函数。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)
。这样,就为装饰器的编写和使用提供了更大的灵活性。
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
上面的use_logging
是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我们使用@use_logging(level="warn")
调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。
类装饰器
再来看看类装饰器,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器还可以依靠类内部的__call__
方法,当使用 @
形式将装饰器附加到函数上时,就会调用此方法。
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
print('class decorator runing')
self._func()
print('class decorator ending')
@Foo
def bar():
print('bar')
bar()
functools.wraps
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__
、参数列表,先看例子:
装饰器:
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
函数:
@logged
def f(x):
"""does some math"""
return x + x * x
该函数完成等价于:
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
不难发现,函数f
被with_logging
取代了,当然它的__name__
、__doc__
就是变成了with_logging
函数的信息了。
print(f.__name__) # prints 'with_logging'
print(f.__doc__) # prints None
这个问题就比较严重的,好在我们有functools.wraps
,wraps
本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,这使得装饰器函数也有和原函数一样的元信息了。
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
print(f.__name__) # prints 'f'
print(f.__doc__) # prints 'does some math'
内置装饰器
@staticmethod
、@classmethod
、@property
装饰器的顺序
@a
@b
@c
def f():
等效于:
f = a(b(c(f)))
python中更优雅的记录日志
在以往我们使用日志,更多的是使用 Python 自带的 logging
模块,它可以设置错误等级、输出方式等。
但使用方式相对比较复杂,想要更好的使用需要如 log4net
一样单独配置,这在 Python 中感觉不是很优雅。
下面介绍一个 Python 库:loguru
。guru 是印度语中大师的意思,loguru
直译就是“日志大师”。
如图 logging
一样,loguru
也有定义日志等级。不同的日志等级,输出效果也不一样(默认的等级由低到高是 DEBUG
、INFO
、WARNING
、ERROR
、CRITICAL
,也可以自己使用 level
函数定义)。
类似 logging
中的 logger.addHandler
,loguru
统一使用 add
函数来管理格式、文件输出、过滤等操作,它提供了许多参数来实现 logger.addHandler
中的配置更加简单方便。
其中 sink
是最重要的参数,可以传入不同的数据类型。传入文件路径、文件句柄、sys.stderr
、甚至 logging
模块的 Handler
如 FileHandler
、StreamHandler
等,这样就可以快速实现自定义的 Handler
配置。
通过给 remove
方法传递 add
方法返回的对象,可以删除 add
方法添加的 sink
,这里的 remove
并不是删除 test2.log
文件,而是停止向该文件输出日志,需要需要继续记录日志则需要重新 add
日志文件。
用 rotation
、retention
、compression
进行日志窗口、更新、压缩管理。
支持控制台输出添加颜色,除了基础色,loguru
甚至允许16进制、RGB格式的颜色值和加粗、下划线等样式。
使用装饰器 @logger.catch
可以和 logging
一样使用 logger.exception
函数来记录异常信息。
使用 exception
方法输出的异常信息包含堆栈信息和当前变量的值,方便问题定位。
使用 serialize
可以将日志转换为 JSON 格式,enqueue
可以保证多线程、多进程安全。
修改时间格式。
Loguru:Python 日志终极解决方案
日志的作用非常重要,日志可以记录用户的操作、程序的异常,还可以为数据分析提供依据,日志的存在意义就是为了能够在程序在运行过程中记录错误,方便维护和调试,能够快速定位出错的地方,减少维护成本。每个程序员都应该知道,不是为了记录日志而记录日志,日志也不是随意记的。要实现能够只通过日志文件还原整个程序执行的过程,达到能透明地看到程序里执行情况,每个线程、每个过程到底执行到哪的目的。日志就像飞机的黑匣子一样,应当能够复原异常的整个现场乃至细节!
最常见的是把输出函数 print()
当作日志记录的方式,直接打印各种提示信息,常见于个人练习项目里,通常是懒得单独配置日志,而且项目太小不需要日志信息,不需要上线,不需要持续运行,完整的项目不推荐直接打印日志信息,现实中也几乎没有人这么做。
我们可以在不少小项目里面看到作者自己写了一个日志模板,通常利用 print()
或者 sys.stdout
稍微封装一下即可实现简单的日志输出,这里的 sys.stdout
是 Python 中的标准输出流,print()
函数是对 sys.stdout
的高级封装,当我们在 Python 中打印对象调用 print(obj)
时候,事实上是调用了 sys.stdout.write(obj+'\n')
,print()
将内容打印到了控制台,然后追加了一个换行符 \n
。
自写日志模板适合比较小的项目,可以按照自己的喜好编写模板,不需要太多复杂配置,方便快捷,但是这种记录日志的方式并不是很规范,有可能你自己觉得阅读体验不错,但是别人在接触你的项目的时候往往需要花费一定的时间去学习日志的逻辑、格式、输出方式等,比较大的项目同样不推荐这种方法。
一个简单的自写日志模板举例:
日志模板 log.py
:
调用日志模块:
日志输出:
在一个完整的项目中,大多数人都会引入专门的日志记录库,而 Python 自带的标准库 logging
就是专门为日志记录而生的,logging
模块定义的函数和类为应用程序和库的开发实现了一个灵活的事件日志系统。由标准库模块提供日志记录 API 的关键好处是所有 Python 模块都可以使用这个日志记录功能。所以,你的应用日志可以将你自己的日志信息与来自第三方模块的信息整合起来。
logging
模块虽然强大,但是其配置也是比较繁琐的,在大型项目中通常需要单独初始化日志、配置日志格式等等,K哥在日常使用中通常都会对 logging
做如下的封装写法,使日志可以按天保存,保留15天的日志,可以配置是否输出到控制台和文件,如下所示:
输出日志:
它在控制台中是这样的:
当然,如果你不需要很复杂的功能,希望简洁一点,仅仅需要在控制台输出一下日志的话,也可以只进行简单的配置:
对于 logging
模块,即便是简单的使用,也需要自己定义格式,这里介绍一个更加优雅、高效、简洁的第三方模块:loguru
,官方的介绍是:Loguru
is a library which aims to bring enjoyable logging in Python. Loguru
旨在为 Python 带来愉快的日志记录。这里引用官方的一个 GIF 来快速演示其功能:
Loguru
仅支持 Python 3.5 及以上的版本,使用 pip 安装即可:
pip install loguru
Loguru
的主要概念是只有一个:logger
控制台输出:
可以看到不需要手动设置,Loguru
会提前配置一些基础信息,自动输出时间、日志级别、模块名、行号等信息,而且根据等级的不同,还自动设置了不同的颜色,方便观察,真正做到了开箱即用!
如果想自定义日志级别,自定义日志格式,保存日志到文件该怎么办?与 logging
模块不同,不需要 Handler
,不需要 Formatter
,只需要一个 add()
函数就可以了,例如我们想把日志储存到文件:
from loguru import logger
logger.add("test.log")
logger.debug("This is a debug message")
我们不需要像 logging
模块一样再声明一个 FileHandler
了,就一行 add()
语句搞定,运行之后会发现目录下 test.log
里面同样出现了刚刚控制台输出的 debug
信息。
与 add()
语句相反,remove()
语句可以删除我们添加的配置:
from loguru import logger
logger.add("test.log")
logger.debug("This is a debug message")
logger.remove(0)
logger.debug("This message won't be written to test.log")
此时控制台会输出两条 debug
信息:
而 test.log
日志文件里面只有一条 debug
信息,原因就在于我们在第二条 debug
语句之前使用了 remove()
语句。
Loguru
对输出到文件的配置有非常强大的支持,比如支持输出到多个文件,分级别分别输出,过大创建新文件,过久自动删除等等。下面我们来详细看一下 add()
语句的详细参数:
基本语法:
logger.add(sink, *, level="DEBUG", format="<green>{time}</green> <level>{message}</level>", filter=None, colorize=None, serialize=False, backtrace=True, diagnose=True, enqueue=False, catch=True)
基本参数释义:
sink
:日志输出的目标,可以是文件路径、文件对象、可调用对象或日志处理器。level
:日志级别,低于该级别的日志不会被记录。format
:日志的格式字符串。filter
:过滤器,用于控制哪些日志消息被记录。colorize
:是否对日志进行颜色化显示。serialize
:是否将日志消息序列化为 JSON 格式。backtrace
:是否显示异常的回溯信息。diagnose
:是否显示异常的诊断信息。enqueue
:是否启用多线程安全的日志记录。catch
:是否捕获日志记录过程中的异常。 当且仅当sink
是协程函数时,以下参数适用:loop
:指定事件循环。 当且仅当sink
是文件路径时,以下参数适用:rotation
:设置日志文件的轮转策略。retention
:设置日志文件的保留策略。compression
:设置日志文件的压缩格式。 这么多参数可以见识到add()
函数的强大之处,仅仅一个函数就能实现logging
模块的诸多功能,接下来介绍几个比较常用的方法。add()
函数的rotation
参数,可以实现按照固定时间创建新的日志文件,比如设置每天 0 点新创建一个 log 文件:
logger.add("file_{time}.log", rotation="00:00")
设置超过 500 MB 新创建一个 log 文件:
logger.add("file_{time}.log", rotation="500 MB")
设置每隔一个周新创建一个 log 文件:
logger.add("file_{time}.log", rotation="weekly")
add()
函数的 retention
参数,可以设置日志的最长保留时间,比如设置日志文件最长保留 15 天:
logger.add("file.log", retention="15 days")
设置日志文件最多保留 10 个:
logger.add("file.log", retention=10)
也可以是一个 datetime.timedelta
对象,比如设置日志文件最多保留 5 个小时:
from datetime import timedelta
logger.add("file.log", retention=timedelta(hours=5))
add()
函数的 compression
参数,可以配置日志文件的压缩格式,这样可以更加节省存储空间,比如设置使用 zip 文件格式保存:
logger.add("file.log", compression="zip")
其格式支持:gz
、bz2
、xz
、lzma
、tar
、tar.gz
、tar.bz2
、tar.xz
Loguru
在输出 log 的时候还提供了非常友好的字符串格式化功能,相当于 str.format()
:
logger.info("My name is {name}, I'm {age} years old.", name="Alice", age=30)
输出:
My name is Alice, I'm 30 years old.
在 Loguru
里可以直接使用它提供的装饰器就可以直接进行异常捕获,而且得到的日志是无比详细的:
@logger.catch
def my_function():
1 / 0
my_function()
日志输出:
Traceback (most recent call last):
File "example.py", line 5, in <module>
my_function()
File "/path/to/loguru/loguru.py", line 945, in catch_wrapper
return function(*args, **kwargs)
File "example.py", line 3, in my_function
1 / 0
ZeroDivisionError: division by zero
在控制台的输出是这样的:
相比 Logging
,Loguru
无论是在配置方面、日志输出样式还是异常追踪,都远优于 Logging
,使用 Loguru
无疑能提升开发人员效率。本文仅介绍了一些常用的方法,想要详细了解可参考 Loguru 官方文档 或关注 Loguru GitHub。
python-复盘-装饰器应用场景大总结
装饰器能有助于检查某个人是否被授权去使用一个 web 应用的端点(endpoint)。它们被大量使用于 Flask 和 Django web 框架中。这里是一个例子来使用基于装饰器的授权:
日志是装饰器运用的另一个亮点。这是个例子:
我敢肯定你已经在思考装饰器的一个其他聪明用法了。
带参数的装饰器是典型的闭包函数(略,参考我之前文章)
我们回到日志的例子,并创建一个包裹函数,能让我们指定一个用于输出的日志文件。
现在我们有了能用于正式环境的 logit
装饰器,但当我们的应用的某些部分还比较脆弱时,异常也许是需要更紧急关注的事情。比方说有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个 email,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。
幸运的是,类也可以用来构建装饰器。那我们现在以一个类而不是一个函数的方式,来重新构建 logit
。
具体再参考我 之前文章,廖神讲解的更清晰
Python笔记:Python装饰器
装饰器是通过装饰器函数修改原函数的一些功能而不需要修改原函数,在很多场景可以用到它,比如① 执行某个测试用例之前,判断是否需要登录或者执行某些特定操作;② 统计某个函数的执行时间;③ 判断输入合法性等。合理使用装饰器可以极大地提高程序的可读性以及运行效率。本文将介绍 Python 装饰器的使用方法。
Python 装饰器可以定义如下:
输出:
Python 解释器将 test_decorator
函数作为参数传递给 my_decorator
函数,并指向了内部函数 wrapper()
,内部函数 wrapper()
又会调用原函数 test_decorator()
,所以 decorator()
的执行会先打印 'this is wrapper'
,然后打印 'hello world'
,test_decorator()
执行完成后,打印 'bye'
,*args
和 **kwargs
,表示接受任意数量和类型的参数。
装饰器 my_decorator()
把真正需要执行的函数 test_decorator()
包裹在其中,并且改变了它的行为,但是原函数 test_decorator()
不变。
一般使用如下形式使用装饰器:
@my_decorator
def test_decorator():
print('hello world')
@my_decorator
就相当于 decorator = my_decorator(test_decorator)
语句。
内置装饰器 @functools.wrap
可用于保留原函数的元信息(将原函数的元信息,拷贝到对应的装饰器函数里)。先来看看没有使用 functools
的情况:
输出:
从上面的输出可以看出 test_decorator()
函数被装饰以后元信息被 wrapper()
函数取代了,可以使用 @functools.wrap
装饰器保留原函数的元信息:
输出:
装饰器可以接受自定义参数。比如定义一个参数来设置装饰器内部函数的执行次数:
输出:
Python 支持多个装饰器嵌套:
装饰的过程:
顺序从里到外:
test_decorator('hello world')
执行顺序和装饰的过程相反。
输出:
类也可以作为装饰器,类装饰器主要依赖 __call__()
方法,是 Python 中所有能被调用的对象具有的内置方法(Python 魔术方法),每当调用一个类的实例时,__call__()
就会被执行一次。
下面的类装饰器实现统计函数执行次数:
输出:
下面介绍两种装饰器使用场景
统计函数执行所花费的时间
输出:
在使用某些 web 服务时,需要先判断用户是否登录,如果没有登录就跳转到登录页面或者提示用户登录:
--THE END--