[Python学习日记-40] 函数进阶之装饰器
简介
引子
什么是装饰器
装饰器终结版
装饰器的层层叠加
简介
在前面铺垫了这么多终于该讲到重点了,前面说的匿名函数、高阶函数、闭包等等都是为了这篇文章所讲的装饰器而使用的,本篇文章将会一一个故事通俗易懂的说明什么是装饰器,那下面我们一起来看看到底什么是装饰器吧
引子
假设你是一家视频网站的后端开发工程师,你们网站有以下几个版块。
def home():print("---首页----")def america():print("----欧美专区----")def japan():print("----日韩专区----")def guangdong():print("----广东专区----")
视频刚上线初期,为了吸引用户,你们采取了免费政策,所有视频免费观看,迅速吸引了一大批用户,免费一段时间后,因为每天巨大的带宽费用公司承受不了了,所以准备对比较受欢迎的几个版块收费,其中包括“欧美”和“广东”专区,当你拿到这个需求后想了想,想收费得先让其进行用户认证,认证通过后,再判定这个用户是否为 VIP 付费会员就可以了,即是 VIP 就让看,不是 VIP 就不让看就行了。 你觉得这个需求太简单了,因为要对多个版块进行认证,那应该把认证功能提取出来单独写个模块,然后每个版块里调用就可以了,与是你轻轻松松的就实现了下面的功能。代码如下
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login():if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Trueelse:print("wrong username or password!")else:print("用户已登录,验证通过...")def home():print("---首页----")def america():login() # 执行前加上验证print("----欧美专区----")def japan():print("----日韩专区----")def guangdong():login() # 执行前加上验证print("----广东专区----")home()
america()
guangdong()
代码输出如下:
你看了看输出,这不正是你想要的效果吗,此时你信心满满的把这个代码提交给你的领导审核,没成想,没过5分钟,代码就被打回来了,领导给你反遗的是:“我们现在有很多模块需要加认证模块,你的代码虽然实现了功能,但是需要更改要加认证的各个模块的源代码,这直接违反了软件开发中的一个原则,即“开放-封闭”原则。简单来说,它规定已经实现的功能代码不应该被修改,但可以被扩展”,即:
- 封闭:已实现的功能代码块不应该被修改
- 开放:对现有功能的扩展开放
作为非科班出身的你对于这个原则是第一次听说,但是不要紧,听到看到就是学到了,那现在应该如何实现这一要求呢?如何在不改原有功能代码的情况下加上认证功能呢?你一时间想不出思路,于是就打开了 CSDN 看起了 JoveZou 写的文章,文章通俗易懂,思路开阔,看着看着一不小心就想到了解决方案。不改源代码可以呀,这个时候可以用前两天在文章里看到的高阶函数(把一个函数当作一个参数传给另一个函数)来解决呀,总算有一天用上它了。这时我只需要写个认证方法,每次调用需要验证的功能时,直接把这个功能的函数名当做一个参数传给我的验证模块不就行了吗,思路弄清楚之后,你噼里啪啦的敲起键盘改写了之前的代码,代码如下
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc()else:print("wrong username or password!")else:print("用户已登录,验证通过...")def home():print("---首页----")def america():print("----欧美专区----")def japan():print("----日韩专区----")def guangdong():print("----广东专区----")home()
login(america) # 需要验证就调用 login,把需要验证的功能当做一个参数传给 login
login(guangdong)
代码输出如下:
你看了看输出结果,很开心,心想终于实现了老板的要求了,不改变原功能代码的前提下,给功能加上了验证。
什么是装饰器
此时你旁边的同事老李刚刚办完事乐呵呵的回到工位上,因为上次他帮了你的大忙,而且你同事还是当年呲碴风云的代码界“浩南哥”,于是你跟他分享了你刚写好的代码,你兴奋的等他看完,等着他夸奖你 NB,没成想老李看完后并没有夸你,而是转过身去笑笑说:“你这个代码还是改改吧,要不然会被开除的。”
你一脸震惊,这明明实现了功能了呀,老李补充到:“没错,你功能是实现了,但是你犯了一个大忌!你改变了调用方式!想一想,现在每个需要认证的模块都必须调用你的 login() 方法,并把自己的函数名传给你,但是人家之前可不是这么调用的,试想一下,如果有100个模块需要认证,那这100个模块都得更改调用方式,这么多模块肯定不止是一个人写的,让每个人再去修改调用方式才能加上认证功能,你会被这些人骂死的。”
你觉得老李说的没错,但是如何即不改变原功能代码,又不改变原有调用方式,还能加上认证呢?你苦思了一会,还是想不出,于是你谦虚的请教了一下老李:“快给我点思路,实在想不出来了。”
老李背对着你问:“学过匿名函数没有?”
你:“学过学过,就是 lambda 嘛。”
老李:“那 lambda 与正常函数的区别是什么?”
你:“最直接的区别是,正常函数定义时需要写名字,但 lambda 不需要。”
老李:“没错,那 lambda 定好后,为了多次调用,可否也给它命个名?”
你:“可以呀,可以写成 plus = lambda x:x+1 类似这样,以后再调用plus就可以了。但这样不就失去了 lambda 的意义了,明明人家叫匿名函数呀,你起了名字有什么用呢?”
老李:“我不是要跟你讨论它的意义,我想通过这个让你明白一个事实。”
老李让你靠边挪挪,做到了你的位置上,写下了下面的代码
def plus(n):return n+1plus2 = lambda x:x+1
老李:“上面两种写法是不是都代表了同样的意思?”
你点了点头,并嗯了一声,老李继续说道:“我给 lambda x:x+1 起了个名字叫 plus2,是不是相当于 def plus2(x)?”
你恍然大悟的说道:“你别说还真是,但老李呀,你想说明什么呢?”
老李:“没啥,只想告诉你,给函数赋值变量名就像 def func_name 是一样的效果的,例如 plus(n) 函数,你调用时可以用 plus 名,甚至还可以再给它起个其它名字,我直接演示一下吧。”于是老李飞快地敲起了代码,没一会就写出了下面的代码
def plus(n):return n+1print(plus(4))
calc = plus
print(calc(4))
代码输出如下:
看着这输出结果,你好像悟道了点什么但是又感觉还是不太明白,老李看着你疑惑的表情,于是他决定再给你点拨一下,于是打开了你之前的代码稍微改了一下,代码如下
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc()else:print("wrong username or password!")else:print("用户已登录,验证通过...")def home():print("---首页----")def america():print("----欧美专区----")def japan():print("----日韩专区----")def guangdong():print("----广东专区----")home()
america = login(america)
guangdong = login(guangdong)
代码输出结果:
看到了输出结果,你疑惑的发现,怎么我还没有调用就已经执行了?你再次疑惑的望向了老李,老李仿佛预知了你的疑惑,他解释道:“先别着急,想要继续下一步要先弄懂这一步的奥秘,代码中很明显是想把 login 的内存地址传给 america 和 guangdong,这样别人如果想要调用的话直接在后面加括号就可以使用了,可是不完美的是,在别人调用之前就已经进行调用了。如果后面再使用 america() 和 guangdong() 来进行调用的话会抛出 TypeError: 'NoneType' object is not callable 的错误。”
你:“老李,你是不是耍我呀,这明显也不行!”
老李:“你先别着急,我都还没说完。调用上面已经符合要求了,那接下来我们要进行调整的就是 login() 里面的了,之所以会直接调用了是因为 america = login(america) 时直接就运行了 login() 内的代码了,这个时候就要用到闭包了。”老李又飞快的改起了代码,代码如下
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):def inner():if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc()else:print("wrong username or password!")else:print("用户已登录,验证通过...")return innerdef home():print("---首页----")def america():print("----欧美专区----")def japan():print("----日韩专区----")def guangdong():print("----广东专区----")home()
america = login(america) # 这次执行 login 返回的是 inner 的内存地址
guangdong = login(guangdong)
america()
代码输出如下:
你看到输出后非常震惊,只需要这么简单的两行代码就完成了?!于是你对老李非常佩服的竖起了大拇指,但老李居然把你的手压了下去,说还没完呢。你很疑惑,这不是已经实现了吗,也没有报错。这个时候老李缓缓说道:“刚刚我们所写的其实就是装饰器,但是我们刚刚所写的都是一些小白写法,如果就这样交上去你还是会被领导一顿臭批,其实还有简写的写法,这也是官方推荐的。”
你:“那应该怎么写呢?”
老李:“只需要在需要登录的版块前面加 @login(login 是扩展的模块)就可以了,那我就演示一下给你看看吧。”于是老李又噼里啪啦的一顿敲键盘,写下了下面的代码
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):def inner():if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc()else:print("wrong username or password!")else:print("用户已登录,验证通过...")return innerdef home():print("---首页----")@login # america = login(america)的简写方法
def america():print("----欧美专区----")def japan():print("----日韩专区----")@login # guangdong = login(guangdong)的简写方法
def guangdong():print("----广东专区----")home()
america()
代码输出如下:
你一看,输出结果,果然和之前写的一样,并且代码变得更加简洁了,心想老李果然 NB 不愧是当年的代码界“浩南哥”,这个时候老李继续说道:“其实这就是我们常说的 Python 中的装饰器了,也叫做语法糖或糖衣语法,英文名叫 Syntactic sugar,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,把装饰器比作糖衣是不是很贴切呢?他就想一层糖衣一样裹在原有的代码外面。其实这只是最基本的装饰器...”
你正听着老李的介绍,突然间你的手机响了一下...
装饰器终结版
你拿起手机一看,是产品部门发来的消息,新的需求要求 VIP 等级达到3级以上才可以看“广东专区”,你看着新的需求思考了一会,用着老李教你的新姿势,在“广东专区”加了个新参数,结果报错了...
这时你赶紧找到了老李问道:“老李,老李,怎么传个参数就不行了呢?”
老李:“那必然呀,你调用 guangdong 时,其实是相当于调用的 login,你的 guangdong 第一次调用时 guangdong = login(guangdong),login 就返回了 inner 的内存地址,第二次用户自己调用 guangdong(5),实际上相当于调用的是 inner,但你的 inner 定义时并没有设置参数,但你给他传了个参数,所以自然就报错了呀。”
你:“但是现在版块需要传参数啊,这样也不行呀...”
老李:“也不是不让你传,只不过要稍做些改动才行。”于是老李又噼里啪啦的敲着键盘,代码如下
试了一下果真好使,真想着感慨老李的姿势高超,花样繁多时,你突然想到,如果需要传输多个参数那应该怎么办呢?于是你再次向老李提问。
老李皱了皱眉说道:“老弟,不是什么都要我教你吧,非固定参数(*args,**kwargs)你没学过吗?”
你:“还能这么搞!NB 我再试试。”最终,你终于搞定了所有需求,完全遵循“开放-封闭”原则,最终代码如下
account = {"is_authenticated":False, # 用户登录了就把这个改成True"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):def inner(*args, **kwargs):if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc(*args, **kwargs)else:print("wrong username or password!")else:print("用户已登录,验证通过...")return innerdef home():print("---首页----")@login # america = login(america)的简写方法
def america():print("----欧美专区----")def japan():print("----日韩专区----")@login # guangdong = login(guangdong)的简写方法
def guangdong(grade):if grade > 3:print("解锁本专区所有高级功能")else:print("----广东VIP专区----")home()
guangdong(5)
america()
代码输出如下:
看着领导空空的工位,不知道他何时偷偷溜走了,于是你兴高采烈的就收拾好东西下班去了,想着明天再交给领导。翌日,你精精神抖擞到了公司,把代码扔给领导,领导看完后,会心一笑说道:“小伙子不错,这才是专业的写法嘛”。
装饰器的层层叠加
没过几天,产品部门又想出了新点子叫着要开会,会议上产品部门想着“广东VIP专区”除了要等级达到3级以上外,还需要再充1000元才能开通。开完会后你坐在自己的工位上陷入了沉思,望了望老李的工位,他今天刚好休假,这该怎么办好呢?突然想起前两天看到的一篇 JoveZou 的博客提到装饰器是可以层层叠加的,于是你就决定要尝试一下用装饰器的叠加来解决该问题,于是你飞快的写下了下面的代码
account = {"is_authenticated":False, # 用户登录了就把这个改成True"is_paid":False, # 假装是从DB获取的付款记录"username":"jove", # 假装这是DB里存的用户信息"password":"abc123" # 假装这是DB里存的用户信息
}def login(func):def inner(*args,**kwargs): # 闭包if account["is_authenticated"] is False:username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("welcome login....")account["is_authenticated"] = Truefunc(*args,**kwargs) # 认证成功,执行功能函数,使用到外部函数的func参数else:print("wrong username or password!")else:print("用户已登录,验证通过...")func(*args,**kwargs) # 认证成功,执行功能函数return innerdef pay_money(func):def inner(*args,**kwargs):if account["is_authenticated"] is False and account["is_paid"] is False:print("this part need pay 1000 yuan.")print("no login,pleace input username and password to pay.")username = input("user:")password = input("pasword:")if username == account["username"] and password == account["password"]:print("pay success...")account["is_authenticated"] = Trueaccount["is_paid"] = Truefunc(*args,**kwargs)else:print("wrong username or password!")elif account["is_authenticated"] is True and account["is_paid"] is False:print("this part need pay 1000 yuan.")print("user is online,please input password to pay.")password = input("pasword:")if password == account["password"]:print("pay success...")account["is_paid"] = Truefunc(*args,**kwargs)else:print("wrong password!")else:print("user is online and paid.")func(*args, **kwargs)return innerdef home():print("---首页----")@login # america = login(america)的简写方法
def america():print("----欧美专区----")def japan():print("----日韩专区----")@pay_money # login = pay_money(login)
@login # guangdong = login(guangdong)的简写方法,装饰器、语法糖
def guangdong(vip_level):if vip_level > 3:print("解锁本专区所有高级功能")else:print("----广东VIP专区----")home()
america() # 相当于执行inner()
guangdong(4)
代码输出如下:
你在原有的代码基础上新加了一个 pay_money(),并且用装饰器加在“广东VIP专区”,你看着自己写出来的代码,又看看了输出,确认这就是你要的,很快在下班前你就交给了你的领导,领导看到后非常满意,并竖起了大拇指表扬你,说你的代码写得非常专业,你明显的感觉到自己有了成长和收获满满,然后你心满意足的下班了,约上前台小姐姐一起去吃晚饭了。