可调用对象
callable() # 可调用的(这个东西加括号可以执行特定的功能,类和函数)
可调用对象即 callable(对象) 返回为 True 的对象
x = 1 print(callable(x)) # False # x() # 会报错,TypeError: 'int' object is not callable def func(): pass print(callable(func)) # True
闭包函数
闭:# 函数内部的函数(全局看不到)
包: # 内部函数引用了外部函数作用域的名字
一个简单的闭包函数案例(无参版)
x = 111 def outter(): x = 100 def inner(): print(x) # 找到的是局部的变量 x return inner # 没有调用,所以没有返回值(函数都没执行) x = 120 res = outter() res() # 100 # 并没有受到全局 x 变化的影响 ---> 上一篇博客名称空间的查找顺序知识点 def func(): x = 666 res() func() # 100 # 并没有受到func空间中 变量x 变化的影响
闭包函数的优点: # 无论在什么地方调用,目标函数(inner) 用的都是外部包围函数outter 中的变量值,不会受全局中变量变化的影响 ---> 可以方便调用函数,一次传参,多次使用
有参版案例
def outter(x, y): # x = 传过来的参数x, y = 穿过来的参数y # x = 1 # y = 40 def my_max(): if x > y: return x return y return my_max res1 = outter(1, 40) # res就是my_max函数的内存地址 print(res1()) print(res1()) # 40 # 40 res2 = outter(90, 200) print(res2()) print(res2()) # 200 # 200 print(res1()) # 40
获取百度首页数据长度(模块知识先不用理解)
普通写法
import requests # 需先安装这个模块 () # 第一个直接给函数传参 url1 = 'https://www.baidu.com' url2 = '...' def my_get(url): response = requests.get(url) if response.status_code == 200: print(len(response.text)) my_get(url1) my_get(url1) my_get('https://www.baidu.com') my_get('https://www.baidu.com') # 2443 # 2443 # 2443 # 2443
闭包传参写法(一次传参即可,避免多次传参)
import requests def outter(url): # url = 'https://www.jd.com' def my_get(): response = requests.get(url) if response.status_code == 200: print(len(response.text)) return my_get my_jd = outter('https://www.jd.com') # 一次传参 my_jd() # 无需传参即可使用 my_jd() my_baidu = outter('https://www.baidu.com') my_baidu() my_baidu() my_jd() # 92775 # 92775 # 2443 # 2443 # 92775
tips: 上面两种写法其实也对应的是给函数传参的两种形式 , # 直接给该函数传参 , # 通过闭包间接达到传参的效果
** 闭包传参就是给要接收参数的函数外层再套一层函数,给外层的函数传参,然后通过函数定义的名称空间查找顺序原理,让该函数获取到外层函数接收到的参数,间接传参,下文的有参数版装饰器用到的就是这个原理
装饰器
初衷(由来):想要在不改变函数源代码且不改变函数调用方式的情况下给函数添加新功能
开放封闭原则: # 对扩展开放,对修改封闭
首先要申明一点: 装饰器真的不难,真的不难,你只要跟着我理解一遍,以后就都不是问题了(忘了你就再看一遍嘛) ,装饰器只是闭包函数的一种扩展应用。
装饰器的推导过程
现有一个需求,给现有的shopping 功能扩展一下,自动判断其是否已经登录,未登录先登录(不改源码与调用方式)
def shopping(): print("我要开始购物啦!") pass
不让改源码又不让改调用方式,那咋整呢?(反正我想了半天是想不出来)
那就一起来头脑风暴一下
先抛开调用方式,我们可以在它调用前后加上自己的逻辑代码,然后封装成函数,通过调用这个函数实现添加功能的目的
def check_login(): if is_login: shopping() else: login() # 调用login 函数,登录,登陆结束后会接着执行shopping 函数 shopping()
如果尝试运用昨天的知识点,函数名可以被当做变量一样赋值传递呢?就是说我把 shopping = check_login ,那是不是说我再 shopping() 执行的就是 check_login() 方法了呢?
is_login = False def shopping(): print("我要开始购物啦!") pass def check_login(): global is_login if is_login: shopping() else: print("登录成功了") # 调用login 函数,登录,登陆结束后会接着执行shopping 函数 is_login = True # 这里写的是 = 赋值操作,局部命名空间会新创建一个变量is_login 而我们要用的是全局的,所以在上方要加上 global is_login shopping() shopping = check_login shopping()
一运行就发现编译器报错了,这样一写原来 check_login() 里面的 shopping 函数 也被顶掉了, 执行的还是 check_login 函数本身 ,它会无限调用自身,然后。。。。
那我可不可以利用 # 名称空间查找顺序在函数定义阶段就已经固定了 的特性,再利用 # 函数名可被当做函数的参数被传递 的特性把 函数名 shopping 作为参数传入进去,然后shopping()的时候调用的是传进去的shopping函数呢?
is_login = False def shopping(): print("我要开始购物啦!") pass def check_login(shopping): global is_login if is_login: shopping() else: print("登录成功了") # 调用login 函数,登录,登陆结束后会接着执行shopping 函数 is_login = True # 这里写的是 = 赋值操作,局部命名空间会新创建一个变量is_login 而我们要用的是全局的,所以在上方要加上 global is_login shopping() shopping = check_login(shopping) shopping() # 登录成功了 # 我要开始购物啦! # Traceback (most recent call last): # File "E:/PyCharm 2019.1.3/ProjectFile/day010/day011/博客代码整理草稿.py", line 20, in <module> # shopping() # TypeError: 'NoneType' object is not callable # tips: 上面的那个报错位置可能会在前两个输出的前面,但这不重要
一运行发现还是报错了,但好像又有点样子了,一经排查,原来在 shopping = check_login(shopping) 的时候就完成了我们的扩展功能。。。报错的是下面那句 shopping() ,前面那句执行了 check_login 函数 ,而 check_login 函数 没有返回值,那么 shopping 变量 接收到的其实是 None ,再执行 shopping() 这就是那个报错的原因了
那我可不可以利用 # 函数在定义阶段只会检查语法,不会执行内部代码 和 # 函数名可以被当做函数的返回值的特性 的特性把 check_login 函数 里面的代码再给它封装一个函数然后返回这个函数名呢?再利用 # 函数名加括号可以调用函数 的特点,要用的时候再给接收的变量加上括号不就可以调用了吗?
is_login = False def shopping(): print("我要开始购物啦!") pass def check_login(shopping): def inner(): global is_login if is_login: shopping() else: print("登录成功了") # 调用login 函数,登录,登陆结束后会接着执行shopping 函数 is_login = True # 这里写的是 = 赋值操作,局部命名空间会新创建一个变量is_login 而我们要用的是全局的,所以在上方要加上 global is_login shopping() return inner shopping = check_login(shopping) shopping() # 登录成功了 # 我要开始购物啦!
经过上述那么一波猛如虎的操作,发现,耶?我好像达到了要求,既没有改变原函数,也没有改变它的调用方式???
那...我要给现有的 pay 功能也同样扩展一下呢?再...?再写一遍?我不!作为一个有追求的程序员,我觉得我的代码还可以抢救一下(不然你让我再给其他功能也同样加上这个登录验证...?那如果有几十个...?想想还是花点脑子写个通用的吧,最起码后面用起来可以省时省事呀,以后也可以模仿着写)
def pay(): print("我要结账啦!") pass
既然先给 pay 函数 也扩展,那我上面的写法里面肯定就不能直接是 shopping() 了,那我再利用一下 # 函数名可以被当做函数的参数被传递 的特性,把.....?等等,函数定义时的形参好像相当于是一个变量我好像直接把上面的 shopping = check_login(shopping) 改成 pay = check_login(pay) 然后 pay() 不就搞定了?一试还真的是这样....
不过啊,这里的shopping 和 pay 函数好像都是没有参数也没有返回值的,那...?要被扩展的函数有参数,或者有返回值呢?那函数的参数又有好几个呢?总不可能每个函数的参数个数都一样吧?这该如何是好?
要可以有返回值,那我调用完被扩展的函数用一个变量接收它的返回值不就行了,再return 出来,哎... 这个简单
那可以有任意个参数呢?emmm? 等等,任意个参数,这好像和可变长参数的应用场景差不多啊,我可以用 # * 接收多余的位置参数, ** 接收多余的关键字参数 ,嘿,这就不管你来几个参数我都可以接收了,那我再通过前面学的打散机制,用 # * 打散容器对象,拆成若干个位置参数, ** 打散字典对象,拆成若干个关键字参数 ,不就完成了参数的传递了?哎,作为一个处女座, check_login 函数 里面接收到的函数名和被执行的函数名怎么能还是shopping 呢?改一下改一下,规范点。
is_login = False def say_hi(username): return 'hello, {}'.format(username) def hello_world(): print('Hello world!') def check_login(func): # 被扩展的函数总不能都叫shopping 吧?取个func 统一代表被扩展的函数吧 # 肯定是调用被扩展函数的时候才根据情况传参数的嘛,所以这里check_login和inner 的参数就得这么写 def inner(*args, **kwargs): # 利用可变长参数接收不定个数的参数(可变长参数的标准写法哦) global is_login if is_login: res = func(*args, **kwargs) # 再利用* ** 的打散机制,将inner 接收到的参数打散 # 用一个变量 res 来接收被扩展函数的返回值 else: print("登录成功了") is_login = True res = func(*args, **kwargs) return res # 再把这个被扩展函数的返回值返回回去 return inner # 返回给外界一个函数名,这样外界就可以拿到这个函数名加括号直接调用了 say_hi = check_login(say_hi) print(say_hi('jason')) # 登录成功了 # hello, jason hello_world = check_login(hello_world) hello_world() # Hello world!
哇塞,完美啊,既可以给有参数的函数调用,又可以给没参数的函数调用,既可以给有返回值的函数调用,又可以给没有返回值的函数调用!NB(到这里,一个简单的装饰器就算是写好了)
但是啊,这个 hello_world = check_login(hello_world) 好像有点多余啊...我不想每次都要写这么一句,哎,python提供的装饰器语法糖了解一下
is_login = False def check_login(func): # 被扩展的函数总不能都叫shopping 吧?取个func 统一代表被扩展的函数吧 # 肯定是调用被扩展函数的时候才根据情况传参数的嘛,所以这里check_login和inner 的参数就得这么写 def inner(*args, **kwargs): # 利用可变长参数接收不定个数的参数(可变长参数的标准写法哦) global is_login if is_login: res = func(*args, **kwargs) # 再利用* ** 的打散机制,将inner 接收到的参数打散 # 用一个变量 res 来接收被扩展函数的返回值 else: print("登录成功了") is_login = True res = func(*args, **kwargs) return res # 再把这个被扩展函数的返回值返回回去 return inner # 返回给外界一个函数名,这样外界就可以拿到这个函数名加括号直接调用了 @check_login # 等价于 say_hi = check_login(say_hi) def say_hi(username): return 'hello, {}'.format(username) @check_login def hello_world(): print('Hello world!') # say_hi = check_login(say_hi) print(say_hi('jason')) # 登录成功了 # hello, jason # hello_world = check_login(hello_world) hello_world() # Hello world!
到了这里,插播一小段装饰器语法糖啊,各位观众姥爷不介意吧?咱们装饰器语法糖之后再见
装饰器语法糖
工作原理: # 装饰器语法糖会自动将下面的可调用对象的名字(函数)当做参数直接传入 @后所跟函数名并自动调用
注意点: # 装饰器语法糖在书写的时候应该与被装饰对象紧紧挨着,中间不能有空行 --> 也就意味着被扩展(装饰)函数要写在装饰器函数的后面,不然装饰器还没定义,你用啥嘞?
案例的话上面的代码就是咯,多多用装饰的语法糖可以让代码的可读性更强哦
装饰器推导过程后续
好,下面我们接着上面的话题继续扯。
这个时候我打印一下 hello_world,我天?怎么是inner ?那我要看下 hello_world 的注释呢?打印一下,耶?也是inner 的,那咋整咧?装饰器修复技术了解一下?
is_login = False def check_login(func): # inner的注释(不给inner函数写函数注释的时候,help(inner) 返回的是这句话,当然这里的inner并没有返回给外界,在全局是获取不到的) def inner(*args, **kwargs): ''' inner的函数注释 :param args: 任意个数的位置参数会被组成一个元组接收 :param kwargs: 任意个数的关键字参数会被组成一个元组接收 :return: 被装饰的函数返回值是什么,这里的返回值就是什么 ''' global is_login if is_login: res = func(*args, **kwargs) # 再利用* ** 的打散机制,将inner 接收到的参数打散 # 用一个变量 res 来接收被扩展函数的返回值 else: print("登录成功了") is_login = True res = func(*args, **kwargs) return res # 再把这个被扩展函数的返回值返回回去 return inner # 返回给外界一个函数名,这样外界就可以拿到这个函数名加括号直接调用了 @check_login # 等价于 say_hi = check_login(say_hi) def say_hi(username): return 'hello, {}'.format(username) @check_login def hello_world(): print('Hello world!') print(hello_world) # <function check_login.<locals>.inner at 0x000002651E63BAE8> print(help(hello_world)) # Help on function inner in module __main__: # # inner(*args, **kwargs) # inner的函数注释 # :param args: 任意个数的位置参数会被组成一个元组接收 # :param kwargs: 任意个数的关键字参数会被组成一个元组接收 # :return: 被装饰的函数返回值是什么,这里的返回值就是什么 # # None
from functools import wraps is_login = False def check_login(func): # inner的注释(不给inner函数写函数注释的时候,help(inner) 返回的是这句话,当然这里的inner并没有返回给外界,在全局是获取不到的) @wraps(func) # 不要忘了导最上面的包,这个语法糖一定要在最内层函数的上一行,然后要指定参数(被装饰方法) def inner(*args, **kwargs): ''' inner的函数注释 :param args: 任意个数的位置参数会被组成一个元组接收 :param kwargs: 任意个数的关键字参数会被组成一个元组接收 :return: 被装饰的函数返回值是什么,这里的返回值就是什么 ''' global is_login if is_login: res = func(*args, **kwargs) # 再利用* ** 的打散机制,将inner 接收到的参数打散 # 用一个变量 res 来接收被扩展函数的返回值 else: print("登录成功了") is_login = True res = func(*args, **kwargs) return res # 再把这个被扩展函数的返回值返回回去 return inner # 返回给外界一个函数名,这样外界就可以拿到这个函数名加括号直接调用了 @check_login # 等价于 say_hi = check_login(say_hi) def say_hi(username): return 'hello, {}'.format(username) @check_login # 测试注释,如果 不写hello_world函数的注释的话,返回的还是inner 函数上方的那行注释(在inner 函数有些函数注释的情况下),这一句注释并不会返回 def hello_world(): ''' hello_world 函数的注释 :return: 木得返回值 ''' print('Hello world!') print(hello_world) # <function hello_world at 0x000001EAA7A50378> print(help(hello_world)) # Help on function hello_world in module __main__: # # hello_world() # hello_world 函数的注释 # :return: 木得返回值 # # None
这一对比,简直就是本质的区别啊
为了将上面花了半天心思推导出来的成果广泛应用,那咱们提炼一个简单的装饰器模板吧?
不带参数的装饰器模板
from functools import wraps # 要用到 wraps 装饰器修复技术,就不要忘了导入这个包 def outter(func): # 这个outter 函数的名字最好取实际用途的名字,比如统计函数运行时间,就可以改成statistical_execution_time @wraps(func) # 加上这句让被装饰函数再被 print(函数名) 的时候可以打印出他自己的内存地址, print(help(函数名)) 的时候可以打印出他自己的注释 def inner(*args, **kwargs): # 这个inner 函数的名字就无所谓啦,没有太大的意义,或者你可以根据你的理解给他取个名字 # 这里写被装饰函数执行前的操作 res = func(*args, **kwargs) # 这里写被装饰函数执行后的操做 return res # 这里不要忘了return 被装饰函数的返回值 return inner # 注意,这里的inner 不能加括号 # 最后一把,在要被他装饰的函数定义位置的上一行加上这个语法糖 --> @outter
咳,编不下去了,这里在扩展一下带参数的装饰器,分析了一下,装饰内部的 inner 函数 参数是可变长度参数,你可以选择在 *args 前面加个位置参数,传入参数,但那就意味着你调用被装饰函数的时候,要多传一个参数,也就意味着调用方式变了,他不再是装饰器了。
那我给最外层的函数多加一个参数呢?
from functools import wraps # 要用到 wraps 装饰器修复技术,就不要忘了导入这个包 def outter(x,func): @wraps(func) def inner(*args, **kwargs): # 这里写被装饰函数执行前的操作 res = func(*args, **kwargs) # 这里写被装饰函数执行后的操做 return res # 这里不要忘了return 被装饰函数的返回值 return inner # 注意,这里的inner 不能加括号 # @outter(1, hello) # 会报错,因为程序执行到这里 hello 函数还没有定义 def hello(): print("hello") hello = outter(1, hello) # 这样写可读性不太好,当然是能用语法糖就语法糖啦 hello() # hello
好像也可以哈? 不过就不能用语法糖了,还是不爽,不能直接传参,那就试试通过闭包传参再套一层呗
带参数的装饰器模板
from functools import wraps # 要用到 wraps 装饰器修复技术,就不要忘了导入这个包 def outs(x): # 其实这里已经可以随便你指定多少个参数了(inner 函数里面可以获取到用了,这里只演示可以传) def outter(func): @wraps(func) def inner(*args, **kwargs): # 这里写被装饰函数执行前的操作 res = func(*args, **kwargs) # 这里写被装饰函数执行后的操做 return res # 这里不要忘了return 被装饰函数的返回值 return inner # 注意,这里的inner 不能加括号 return outter # 这里这个return 函数名 也不要忘了,记住一点,函数名后面千万不要加括号!函数名后面千万不要加括号!函数名后面千万不要加括号! # 至此,带参数版的装饰器差不多就写好了,outs函数里的 x 根据情况换成需要的参数(如果你不需要参数,那你写简单版的不好吗?) @outs(1) # 这里的outs 要加括号!直接执行 outs 函数,然后返回 outter函数内存地址,传入hello 函数名(对象),装饰 # --> outs() -> hello = outter(hello) def hello(): print("hello") # 上面装饰器语法糖的写法等同于: # outter = outs(1) # hello = outter(hello) hello() # hello
emmm,到这里就差不多了,完事儿,收工。
咳,申明一点啊,装饰器可不是我推导出来的,写上面那一长串的知识点呢,主要是为了真正了解装饰器,并且复习一下前面的知识点嘛
下面通过一个案例加深一下对装饰器的理解(多层装饰器)
def outter1(func1): print('语法糖加载outter1') def wrapper1(*args, **kwargs): print('被装饰函数函数名加括号调用,执行到了wrapper1') res1 = func1(*args, **kwargs) return res1 return wrapper1 def outter2(func2): print('语法糖加载outter2') def wrapper2(*args, **kwargs): print('被装饰函数函数名加括号调用,执行到了wrapper2') res2 = func2(*args, **kwargs) return res2 return wrapper2 def outter3(func3): print('语法糖加载outter3') def wrapper3(*args, **kwargs): print('被装饰函数函数名加括号调用,执行到了wrapper3') res3 = func3(*args, **kwargs) return res3 return wrapper3 @outter1 # index = outter1(wapper2) # 完成装饰 @outter2 # wrapper2 = outter2(wrapper3) @outter3 # wrapper3 = outter3(最原始的index函数内存地址) # 语法糖加载outter3 # 只要用语法糖装饰到函数上就会返回 # 语法糖加载outter2 # 语法糖加载outter1 def index(): print('from index') index() # 被装饰函数函数名加括号调用,执行到了wrapper1 # 被装饰函数函数名加括号调用,执行到了wrapper2 # 被装饰函数函数名加括号调用,执行到了wrapper3 # from index
从上面的案例中也可以总结出: # 多个装饰器装饰函数时顺序是从下往上的,执行顺序是从上往下的
怎么样,不难吧?理解了再看一遍,你会发现好简单啊