python装饰器全解

  • 引言
  • AOP(面向切片编程)
  • python装饰器实现
    • 装饰器实例
    • 时间控制
    • 带参数的装饰器
    • 异常处理
    • 日志处理
    • 连接重试
    • url去重
    • 权限认证
    • aop开关代理
  • 总结

引言

装饰器(Decorators)在python里面是一个重要部分,但具体重要在哪,可能各种视频里也只是出现在python函数进阶里会提及,然而从进入网络甚至到框架很多时候就并不需要在对这个概念有所深入,我认为其原因可能是python比较主流的框架,如Django和flask都对其做了封装,Django最是彻底,从异常到权限认证等都不需要动脑,已经"保姆式",这也是我想写本篇的目的,包括我自己,已经很久没有用自己封装的装饰器了,但并不妨碍本篇想总结一下这些个概念。

AOP(面向切片编程)

AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
在不改变原有的逻辑的基础上,增加一些额外的功能。代理也是这个功能,读写分离也能用aop来做。
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

https://baike.baidu.com/item/AOP/1332219?fr=aladdin

这是我在Java的spring中有提到的概念,来源于百度百科。而我们能从中总结出AOP的使用场景如下:
python装饰器全解_第1张图片

另外还有一些更深入的理解,可以去看看Java的spring底层,或者flask中的蓝图还有jwt等实现过程,Django因为年代太久远了,我刚开始写的关于Django博客,目前我自己也很多都忘了,而我在flask中有用到的装饰器会在之后说明,那么接下来,就是python的装饰器的实现方案。

python装饰器实现

这里的很多代码以及说明会参照菜鸟教程的案例,和当初看老男孩记录的关于装饰器的笔记,本篇我也是我想将之前很多东西都总结起来,把写的和看到的都串联起来,下图为很久之前画的图:python装饰器全解_第2张图片

装饰器实例

# 装饰器进阶
    # functools wraps
    # 带参数的装饰器
    # 多个装饰器装饰同一个函数
# 周末的作业(用装饰器)
    # 文件操作
    # 字符串处理
    # 输入输出
    # 流程控制

# 装饰器
# 开发原则: 开放封闭原则
# 装饰器的作用 : 在不改变原函数的调用方式的情况下,在函数的前后添加功能
# 装饰器的本质: 闭包函数

def wrapper(func):
    def inner(*args,**kwargs):
        ret = func(*args,**kwargs)
        return ret
    return inner

@wrapper    # 做的事情是:holiday = wrapper(holiday)
def holiday(day):
    print("放假了%s"%day)

rat = holiday(3)
print(rat)
"""
放假了3
None
"""

上述即为最简单的一个装饰器实例,这里切记一点的是,最后一个return需要返回函数本身,而不是函数调用,不然下面的holiday接收到的值将为None(),这将会报错 TypeError: holiday() missing 1 required positional argument: ‘day’,把一对小括号放在后面,这个函数就会执行;如果不放括号在它后面,那它可以被到处传递,并且可以赋值给别的变量而不去执行它,这也是闭包的其中一个概念。对这个概念有一篇比较好的博文推荐为:开放封闭原则(Open Closed Principle)

时间控制

def deco(func):
    def wrapper(*args, **kwargs):
        startTime = time.time()
        func(*args, **kwargs)
        endTime = time.time()
        msecs = (endTime - startTime)*1000
        print("time is %d ms" %msecs)
    return wrapper


@deco
def func(a,b):
    print("hello,here is a func for add :")
    time.sleep(1)
    print("result is %d" %(a+b))

func(4,5)
"""
hello,here is a func for add :
result is 9
time is 1000 ms
"""

可能一般最开始接触到的装饰器会是判断函数运行时间的逻辑,因为第一是直观,第二是比较简单,所以如果有很多个函数需要被等待执行,我们不想每个函数内部都做修改,就可以运用如上装饰器对每个函数进行使用。

带参数的装饰器

那么当我们做好一个装饰器,又有新的功能过来,我们可能会做很多的装饰器,当装饰器很多时,我们难免会很难找装饰器或者要通过不同的文件找到我们需要的东西,所以我们想要有一个办法,能一下子就知道它有还是没有,执行或者没有被执行。

import time
FLAGE = False
def timmer_out(flag):
    def timmer(func):
        def inner(*args,**kwargs):
            if flag:
                start = time.time()
                ret = func(*args,**kwargs)
                end = time.time()
                print(end-start)
                return ret
            else:
                ret = func(*args, **kwargs)
                return ret
        return inner
    return timmer
# timmer = timmer_out(FLAGE)
@timmer_out(FLAGE)    #wahaha = timmer(wahaha)
def wahaha():
    time.sleep(0.1)
    print('wahahahahahaha')

@timmer_out(FLAGE)
def erguotou():
    time.sleep(0.1)
    print('erguotoutoutou')

wahaha()
erguotou()

而上面代码的意思即为在已有基础上,我们可以定义一个新函数来控制它(定义函数最多用三层嵌套为好)此时的@timer_out(FLAGE)传参数,只是为了比原来的好接收,但不影响其它地方。@time_out(FLAGE)还能有另一种写法:和上图一样,time_out(FLAGE)的意思是首先把time_out(FLAGE)赋给timer,即timer = time_out(FLAGE),然后再用timer的语法糖的功能。这种方法叫带参数的装饰器。

异常处理

from functools import wraps

def simpleDecorator(b_func):
    @wraps(b_func)
    def wrapBFuction(*args,**kwargs):
        print("begining records")
        dd = b_func(*args,**kwargs)
        if dd:
            return dd
        print("function can't run")
    return wrapBFuction

@simpleDecorator
def bFunction(test_dict):
    return test_dict


test_dict = {
     "a":1,"b":2}
aac = {
     }
b = bFunction(test_dict)
print(b)
b = bFunction(aac)
print(b)

"""
begining records
1111
{'a': 1, 'b': 2}
begining records
function can't run
None
"""

可以看到当我们装饰的函数如果返回值是空的,将会报function can’t run,这个可以直接写成异常,或者直接将装饰器写成异常类:

class testException(Exception):
	def __init__(self, error_message):
		self.error_message = error_message

	def __str__(self):
		return "Received an exception as {}" .format(self.error_message)

	def __erpr__(self):
		return self.error_message


# 定义异常捕获处理装饰器
def exc_handler(func):
	def inner(*args, **kwargs):
		try:
			return func(*args, **kwargs)
		except testException:
			return "捕获到自定义异常"
		except KeyError:
			return "捕获到键值异常"
		except BaseException:
			return "捕获到其他异常"
	return inner

日志处理

下面的第一段代码来自 装饰器记录日志:

from functools import wraps
import inspect
import logging


# 创建logger
logger = logging.getLogger('func_log')
logger.setLevel(logging.DEBUG)

# 写入日志
fh = logging.FileHandler('test.log')
fh.setLevel(logging.DEBUG)

# 输出控制台
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# handler输出格式
formatter = logging.Formatter('[%(asctime)s][%(thread)d][%(filename)s][line: %(lineno)d][%(levelname)s] ## %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)

# 绑定handler
logger.addHandler(fh)
logger.addHandler(ch)


# 装饰器:打印函数名并写入日志
def decorator(function):
    @wraps(function)
    def inner(*args, **kwargs):
        result = function()
        logger.debug('%s is excuted!'%function.__name__)
        return result
    return inner

@decorator
def func1():
    print('%s is excuted'% inspect.stack()[0][3])  #函数内部打印方式  调用inspect模块(可注释)
    

@decorator
def func2():
    pass

if __name__ == '__main__':
    func1()
    func2()

在这里插入图片描述

这段代码可以直接运行,并将产生一个test.log的文件于项目的根目录,内容就是调用成功,整段代码理解起来并不难,因为是面向过程,这却相对静态,因为文件名以及很多条件都是需要在后续动态修改的,所以还是得面向对象,那么可以看菜鸟中的代码为:

from functools import wraps
 
class logit(object):
    def __init__(self, logfile='out.log'):
        self.logfile = logfile
 
    def __call__(self, func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            # 打开logfile并写入
            with open(self.logfile, 'a') as opened_file:
                # 现在将日志打到指定的文件
                opened_file.write(log_string + '\n')
            # 现在,发送一个通知
            self.notify()
            return func(*args, **kwargs)
        return wrapped_function
 
    def notify(self):
        # logit只打日志,不做别的
        pass

@logit()
def myfunc1():
    pass

那么,根据相应的业务,还有可以将logfile变为参数,那么就能在同一文件下的不同函数,将日志都写进同一个log。

连接重试

这里我们可以自定义重试函数,也能利用官方写好的一个包,叫retry,那么先介绍自定义重试函数:

import functools
import time


# 最大重试次数/重试间隔(单位秒)
def retry(stop_max_attempt_number=10, wait_fixed=2):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            retry_num = 0
            while retry_num < stop_max_attempt_number:
                rs = None
                try:
                    rs = func(*args, **kw)
                    break
                except Exception as e:
                    retry_num += 1
                    time.sleep(wait_fixed)
                    if retry_num == stop_max_attempt_number:
                        raise Exception(e)
                finally:
                    if rs:
                        return rs

        return wrapper
    return decorator

引用自:https://www.cnblogs.com/wangbin2188/p/13129748.html

可以看到上面的例子还是比较完善的,将次数和间隔都考虑了进去,所以稍微修改一下,可以测试一下重连:

import functools,time


def retry(stop_max_attempt_number=10, wait_fixed=0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            retry_num = 0
            while retry_num < stop_max_attempt_number:
                rs = None
                try:
                    rs = func(*args, **kw)
                    # break
                except Exception as e:
                    retry_num += 1
                    time.sleep(wait_fixed)
                    print(e)
                    if retry_num == stop_max_attempt_number:
                        print("end")
                finally:
                    if rs:
                        return rs

        return wrapper
    return decorator

@retry(5)
def get_data(url):
    r = requests.get(url, timeout=2)
    return r.status_code

print(get_data("http://127.0.0.1"))

"""
HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(, 'Connection to 127.0.0.1 timed out. (connect timeout=2)'))
HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(, 'Connection to 127.0.0.1 timed out. (connect timeout=2)'))
HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(, 'Connection to 127.0.0.1 timed out. (connect timeout=2)'))
HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(, 'Connection to 127.0.0.1 timed out. (connect timeout=2)'))
HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(, 'Connection to 127.0.0.1 timed out. (connect timeout=2)'))
end
None
"""

而同样,python也推出了异常重试包,为retry,这个包的功能更加齐全,基本上能想到的异常类都在其内部有体现,如果不是业务有什么特殊的逻辑,比如说在异常重连的过程内,还需要添加日志通过TCP或者UDP发送错误至ELK平台,之前我也只是测试过这样做,但我感觉对于解耦来讲,这样修改其实也还是面向对象的日志类,就没有往下面继续想了,那么说回正题,pip下好包后,点进去就有整个retry的所有参数,这个包是很开门见山也没法遮遮掩掩的:
python装饰器全解_第3张图片
具体参数解释和案例可以看 官网:https://tenacity.readthedocs.io/en/latest/,这里就不再举例了。

url去重

没错,上面的重连已经提到了requests还有TCP和UDP发送ELK日志,这如果是请求成功了,或许就不需要写进ELK日志,但数据的增多,就会出现业务上的变动,在爬虫领域,用redis去重是很常见的,我们可以利用其进行去重:

def get_redis():
    pool = redis.ConnectionPool(host='xxxx', port=6379)
    conn = redis.Redis(connection_pool=pool)
    return conn


def url_filter(web_name):
    def decorator(func):
        def wrapper(*args):
            redis_conn = get_redis()
            url = args[0]
            md5_url = hashlib.md5(url).hexdigest()
            if not redis_conn.sismember(web_name, md5_url):
                res = func(*args)
                if res:
                    redis_conn.sadd(web_name, md5_url)
        return wrapper
    return decorator

@url_filter('block')
def get_project_message(url, data):
    r = session.get(url)
    if r.status_code == 200:
        parse_project_message(r.content, data)


def add_user(uid, user_id):
    redis_conn.incr(f"counter_{user_id}")
    redis_conn.lpush(f"user_{uid}", str(user_id))
    redis_conn.sadd(f"all_uids", str(uid))
    redis_conn.zincrby("article_hots", 1, str(user_id))


if __name__ == '__main__':
    for key in redis_conn.keys():
        redis_conn.delete(key)
	add_user()
	get_project_message()

里面的装饰器引用自:初识 python 装饰器 以及 通过url去重,然后我加了几个函数进去,不过我发现写到现在好像时间有点晚了,我是一边以记笔记的方式,再边找资料,之后有时间再找几个url测试一下,现在一时半会儿想不到了。。

权限认证

关于这个,菜鸟教程有一个权限的模板为:

from functools import wraps
 
def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            authenticate()
        return f(*args, **kwargs)
    return decorated

而在Django和flask中,也存在大量的权限类的装饰器,这里列出我找到的一些包目录:

from django.contrib.auth import  authenticate, login, logout
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, PermissionsMixin
from django.contrib.auth import get_user_model

不得不说Django的权限系统做得非常完美,在前后端不分离的时期,基本属于霸主级地位了,MVT的架构设计确实让用户不再纠结于自造轮子,而只需加以自己的业务逻辑外加调用上述的这些api就能很轻松的完成一个项目的构建,我这里也没有再找一些自定义的权限认证装饰器类,因为我感觉,不如深入源码,它的内部肯定比其它方式更加详细。

而在前后端分离的时代,Django内部的auth模块确实用得很少了,转而是flask中的jwt用得很多,虽然说Django推出了djangorestframework,并且内部的源代码很强,我之前刚开始写博客便是从drf入手,这份代码可以说化繁为简,不论大神还是小白都能看到自己想看的,不得不佩服。

flask的jwt版本可以去官网查看:https://readthedocs.org/projects/flask-jwt-extended/

而Django的jwt,可以看我之前写过的一篇博客,目前很多我也忘了:restframework(4):JWT

aop开关代理

def degrade(app):

    def _wrapper(function):

        def __wrapper(*args, **kwargs):

            value = None
            try:
                redis = codis_pool.get_connection()
                value = redis.get("dmonitor:degrade:%s" % app)
            except Exception, _:
                logger.info(traceback.format_exc())

            if not value or int(value) != 1:
                function()
                logger.info("[degrade] is_on: %s" % app)
            else:
                logger.info("[degrade] is_off: %s" % app)
        return __wrapper

    return _wrapper

@degrade
def do(app):
    # do whatever
    pass

引用自:python语言中的AOP利器:装饰器

只要do函数被@degrade装饰,就会安装app名称校验redis里的开关,一旦发现开关关闭,则do函数不被执行,也就是降级。这算是上面的一个功能集合,避免了重连,也解决了动态开关的机制,同样还做了相应的日志记录。

总结

其实还有很多种类的我这里没有提到,比如说还有的功能为 类型检查,字段补充 等等,装饰器锁涉及到的东西太广,也能走得很深,关键看怎么用,上面思维导图里提到的懒加载还有事务等几个我确实没有看到过,后续如果有从Java或者学习过程中接触到,会回来补充完这部分内容。

你可能感兴趣的:(python,aop,python,装饰器)