Python三大器之装饰器
开放封闭原则
一个良好的项目必定是遵守了开放封闭原则的,就比如一段好的Python代码必定是遵循PEP8
规范一样。那么什么是开放封闭原则?具体表现在那些点?
开放封闭原则的核心的思想是软件实体是可扩展,而不可修改的。 也就是说,对扩展是开放的,而对修改是封闭的。 即使迫不得已要进行修改,也最好不要改变它原本的代码。
具体表现的点:
1.写好的项目,后期可以很方便的为其添加新的功能
2.项目修改应该尽量少的改动原本逻辑代码。而是通过某种补丁的形式完善其功能
初识装饰器
什么是装饰器
装饰器应该拆开来进行讲解:
"器"指的是器具,工具。可以理解为函数
"装饰"指的是为其他事物添加额外的东西点缀。
"装饰器"就是指定义一个函数,而该函数的主要作用便是用于为其他函数做功能上的一个补充(添加新的功能)。
为什么要有装饰器
补充一个函数的功能非常简单。但是要在遵循开放封闭原则的前提下为一个函数做功能的补充就不是那么简单了,装饰器就是为了解决这种场景而诞生的。
装饰器可以在遵循开放封闭原则的前提下为被装饰对象添加新的功能(即不修改源代码及其调用方式的前提下为其添加新功能)
怎么使用装饰器
装饰器说白了就是闭包函数加上*args
以及**kwargs
的一个综合应用。下面的内容将带你深入浅出的了解装饰器的定义与使用。
无参装饰器实现
需求分析
import time import random # 通过time模块和random模块模拟下载,上传功能所花费的时间 # 以下两个函数都部署在线上服务器上: # 要求为其增添新的功能,不违反开放封闭性原则的前提下统计下载和上传花费的时长。 # (即:不修改download和upload的调用方式和源码的情况下为其添加统计时长的功能) def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") download("www.cnblogs.com/xxx/file/x.jpg") download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img")
解决方案一
既然要在不修改源码与调用方式的前提下增添新功能。那么我们可以这么做:
import time import random # 通过time模块和random模块模拟下载,上传功能所花费的时间 # 以下两个函数都部署在线上服务器上: # 要求为其增添新的功能,不违反开放封闭性原则的前提下统计下载和上传花费的时长。 # (即:不修改download和upload的调用方式和源码的情况下为其添加统计时长的功能) def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") start = time.time() download("www.cnblogs.com/xxx/file/x.jpg") end = time.time() print("下载www.cnblogs.com/xxx/file/x.jpg资源所耗费的时长为:{0:.2f}秒".format(end-start)) start = time.time() download("www.cnblogs.com/yyy/file/y.jpg") end = time.time() print("下载www.cnblogs.com/yyy/file/y.jpg资源所耗费的时长为:{0:.2f}秒".format(end-start)) start = time.time() upload("www.cnblogs.com/zzz/file/img") end = time.time() print("上传资源至www.cnblogs.com/zzz/file/img所耗费的时长为:{0:.2f}秒".format(end-start)) # ==== 执行结果 === """ 正在下载www.cnblogs.com/xxx/file/x.jpg的资源... 下载完成... 下载www.cnblogs.com/xxx/file/x.jpg资源所耗费的时长为:3.00秒 正在下载www.cnblogs.com/yyy/file/y.jpg的资源... 下载完成... 下载www.cnblogs.com/yyy/file/y.jpg资源所耗费的时长为:1.00秒 正在向www.cnblogs.com/zzz/file/img上传资源... 上传完成... 上传资源至www.cnblogs.com/zzz/file/img所耗费的时长为:1.00秒 """
问题解决,不修改源代码以及调用方式的前提下为download
与upload
函数添加好了新功能,但是这种解决方案是非常愚蠢的。
写重复代码,程序可读性非常差而且非常耗费人力去单纯的做复制粘贴。
解决方案二
import time import random # 通过time模块和random模块模拟下载,上传功能所花费的时间 # 以下两个函数都部署在线上服务器上: # 要求为其增添新的功能,不违反开放封闭性原则的前提下统计下载和上传花费的时长。 # (即:不修改download和upload的调用方式和源码的情况下为其添加统计时长的功能) def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") def outer(func): # func 是函数的内存地址 def warpper(url): start = time.time() func(url) # 这里才是真正运行的 download 或者 upload 函数。 end = time.time() if func.__name__ == "download": # func.__name__ 可以拿到函数名 print("下载{0}资源所耗费的时长为:{1:.2f}秒".format(url, (end - start))) else: print("上传资源至{0}所耗费的时长为:{1:.2f}秒".format(url, (end - start))) return warpper # download函数内存地址传递进去。 变量值download接收,返回的是 warpper 函数的内存地址。此download非彼download download = outer(download) upload = outer(upload) download("www.cnblogs.com/xxx/file/x.jpg") # 由于warpper指向的函数需要一个参数url,所以直接传即可。 download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img") # ==== 执行结果 === """ 正在下载www.cnblogs.com/xxx/file/x.jpg的资源... 下载完成... 下载www.cnblogs.com/xxx/file/x.jpg资源所耗费的时长为:1.02秒 正在下载www.cnblogs.com/yyy/file/y.jpg的资源... 下载完成... 下载www.cnblogs.com/yyy/file/y.jpg资源所耗费的时长为:2.01秒 正在向www.cnblogs.com/zzz/file/img上传资源... 上传完成... 上传资源至www.cnblogs.com/zzz/file/img所耗费的时长为:2.02秒 """
解决方案二已经完美完成需求了。但是还能再做一些修改,基于开放封闭原则,该解决方案的扩展性还不是很强,如果后期upload
或者download
的源码真的发生改变所需要的参数增多了。那我们的这个outer
函数也需要去做改变,很麻烦。所以我们需要在此基础上做一个改进。
解决方案三
import time import random # 通过time模块和random模块模拟下载,上传功能所花费的时间 # 以下两个函数都部署在线上服务器上: # 要求为其增添新的功能,不违反开放封闭性原则的前提下统计下载和上传花费的时长。 # (即:不修改download和upload的调用方式和源码的情况下为其添加统计时长的功能) def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") def outer(func): # func 是函数的内存地址 def warpper(*args, **kwargs): # 优化1:参数转接,warpper直接对接func,多少参数都没问题。取决于func start = time.time() # 这里才是真正运行的 download 或者 upload 函数。 优化2:如果函数有了返回值,我们就可以对他进行返回 res = func(*args, **kwargs) end = time.time() if func.__name__ == "download": # func.__name__ 可以拿到函数名 # 优化3:这里改成args[0]即可。位置传参url必须一一对应,其实有个小bug,关键字传参有问题。 print("下载{0}资源所耗费的时长为:{1:.2f}秒".format(args[0], (end - start))) else: print("上传资源至{0}所耗费的时长为:{1:.2f}秒".format(args[0], (end - start))) return res # 优化2:返回func的返回值 return warpper # download函数内存地址传递进去。 变量值download接收,返回的是 warpper 函数的内存地址。此download非彼download download = outer(download) upload = outer(upload) download("www.cnblogs.com/xxx/file/x.jpg") # 由于warpper指向的函数需要一个参数url,所以直接传即可。 download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img") # ==== 执行结果 === """ 正在下载www.cnblogs.com/xxx/file/x.jpg的资源... 下载完成... 下载www.cnblogs.com/xxx/file/x.jpg资源所耗费的时长为:1.02秒 正在下载www.cnblogs.com/yyy/file/y.jpg的资源... 下载完成... 下载www.cnblogs.com/yyy/file/y.jpg资源所耗费的时长为:2.01秒 正在向www.cnblogs.com/zzz/file/img上传资源... 上传完成... 上传资源至www.cnblogs.com/zzz/file/img所耗费的时长为:2.02秒 """
无参装饰器模板
# ==== 无参普通方式调用 ==== def test(x, y, z): print(x, y, z) return "结果" def outer(func): # 装饰器名字应该取好,func代表被装饰函数。 def warpper(*args, **kwargs): # warpper函数对应func,也就是被执行的函数。这里做对接 # 功能扩展区,函数运行前 res = func(*args, **kwargs) # 接收func的return值 # 功能扩展区,函数运行后 return res # 将func的返回值返回出去 return warpper test = outer(test) # test变量名执行warpper内存地址。 res = test(1, 2, 3) # 拿到warpper的return值,看起来在执行test,实际上在执行warpper print(res)
# ==== 无参wraps普通方式调用 ==== from functools import wraps def test(x, y, z): print(x, y, z) return "结果" def outer(func): # 装饰器名字应该取好,func代表被装饰函数。 @wraps(func) # 这里主要完全将warpper的所有文档信息等等都与func保持一致。下面会讲 def warpper(*args, **kwargs): # warpper函数对应func,也就是被执行的函数。这里做对接 # 功能扩展区,函数运行前 res = func(*args, **kwargs) # 接收func的return值 # 功能扩展区,函数运行后 return res # 将func的返回值返回出去 return warpper test = outer(test) res = test(1, 2, 3) # 拿到warpper的return值 print(res)
# ==== 无参wraps@语法糖方式调用 ==== from functools import wraps def outer(func): # 装饰器名字应该取好,func代表被装饰函数。 @wraps(func) # 这里主要完全将warpper的所有文档信息等等都与func保持一致。下面会讲 def warpper(*args, **kwargs): # warpper函数对应func,也就是被执行的函数。这里做对接 # 功能扩展区,函数运行前 res = func(*args, **kwargs) # 接收func的return值 # 功能扩展区,函数运行后 return res # 将func的返回值返回出去 return warpper @outer # 将下面一坨被装饰函数放入func中。相当于 --> test = outer(test)。此时执行test就相当于执行warpper。 def test(x, y, z): print(x, y, z) return "结果" res = test(1, 2, 3) # 拿到warpper的return值 print(res)
(对于
@
语法糖调用形式来说):outer
层的参数只应该有一个func
形参来接收被装饰的函数对象,wapper
层就只该有*args
和**kwargs
,为的是和传入的func
(函数对象)做参数对接,参数不能多传递。
@语法糖
@调用装饰器
上面的第三种解决方案其实就是装饰器。那么我们可以看到他还是有一些复杂的地方,比如:
其实Python为了简化这一步骤。提供了@
语法糖,我们看一下它是如何使用的。
# ==== @语法糖的使用 ==== # 第一步:装饰器所有代码拖上来,拖到被装饰对象的上面。 def outer(func): # func 是函数的内存地址 def warpper(*args, **kwargs): # 优化1:参数转接,warpper直接对接func,多少参数都没问题。取决于func start = time.time() # 这里才是真正运行的 download 或者 upload 函数。 优化2:如果函数有了返回值,我们就可以对他进行返回 res = func(*args, **kwargs) end = time.time() if func.__name__ == "download": # func.__name__ 可以拿到函数名 # 优化3:这里改成args[0]即可。位置传参必须一一对应,其实有个小bug,关键字传参有问题。 print("下载{0}资源所耗费的时长为:{1:.2f}秒".format(args[0], (end - start))) else: print("上传资源至{0}所耗费的时长为:{1:.2f}秒".format(*args[0], (end - start))) return res # 优化2:返回func的返回值 return warpper # 第二步:@后面跟上装饰器名称, # 其实@outer就相当于把下面一坨被装饰函数当做参数传给outer --> download = outer(download)。而下面执行download的时候相当于就是执行返回值warpper函数 @outer def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") @outer def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") # 第三步: 删除偷梁换柱的操作 download("www.cnblogs.com/xxx/file/x.jpg") # 由于warpper指向的函数需要一个参数url,所以直接传即可。 download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img")
@到底做了什么
想要了解@
到底做了什么,先介绍一个内置函数叫做help()
,它可以获得一些对于当前函数的帮助信息。
# ==== @内部做了那些事 ==== def test(): """这是一个测试""" pass res = help(test) # 获得帮助。 # help的返回值是None,并且help会自动调用print打印出结果。注意!!! print(res) # 这里只是打印help方法的返回值。 # ==== 执行结果 ==== """ Help on function test in module __main__: test() 这是一个测试 None # <--这里是print的结果,上面全是help的结果,help内部自动调用print方法打印出了上面的信息 """
我们再来测试看结果:
# ==== @内部做了那些事 ==== @help # 由于help内部会自动调用print,所以会先出现它的信息 def test(): """这是一个测试""" pass test() # ==== 执行结果 ==== """ Help on function test in module __main__: test() 这是一个测试 Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/FunctionLearn.py", line 82, in <module> test() TypeError: 'NoneType' object is not callable # <-- 注意,意思是说空类型不可被调用的意思 """
结论如下:
# ==== @内部做了那些事(结论) ==== # @后面跟着函数名。会自动将被装饰函数当做参数拿进来(如果手动加括号,被装饰函数就不能灵活的传进来,这是一种限制),help(test),并且会将它返回给变量test,那么便是 test = help(test)... # 由于help的返回值为None,所以... @help def test(): """这是一个测试""" pass test() # 现在的test,就是help的返回结果。也就是None,一个名字加括号代表要执行它。而None类型不可被执行。 # ==== 执行结果 ==== """ Help on function test in module __main__: test() 这是一个测试 Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/FunctionLearn.py", line 82, in <module> test() TypeError: 'NoneType' object is not callable """
现在我们知道了。
@
会自动调用的()
,并且将下面的被装饰函数当做参数传递进去,如果我们手动做这件事会发生什么呢?依旧会自动运行,但是不会再将被装饰函数当做参数传入,括号传入的是是什么就是什么,也就是说它最多只能接收一个实参传递。
# ==== @手动传参 ==== @help(print) def test(): """这是一个测试""" pass test() # 现在的test,就是help的返回结果。也就是None,一个名字加括号代表要执行它。而None类型不可被执行。 # ==== 执行结果 ==== Ps:可以看到,它打印出了print的帮助信息。而test的帮助信息并没有传递进去。 """ Help on built-in function print in module builtins: print(...) print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False) Prints the values to a stream, or to sys.stdout by default. Optional keyword arguments: file: a file-like object (stream); defaults to the current sys.stdout. sep: string inserted between values, default a space. end: string appended after the last value, default a newline. flush: whether to forcibly flush the stream. Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/FunctionLearn.py", line 80, in <module> def test(): TypeError: 'NoneType' object is not callable """
多个@装饰器分析
如果一次性使用多个@装饰器,会发生什么?
记住一点:
多个装饰器的执行顺序自下而上
# ==== 多个@装饰器执行顺序 ==== def f1(func): print("执行f1装饰器了") def warpper(*args, **kwargs): print("我是f1中的warpper.我的参数是..", func) res = func(*args, **kwargs) return res return warpper def f2(func): print("执行f2装饰器了") def warpper(*args, **kwargs): print("我是f2中的warpper.我的参数是..", func) res = func(*args, **kwargs) return res return warpper def f3(func): print("执行f3装饰器了") def warpper(*args, **kwargs): print("我是f3中的warpper.我的参数是..", func) res = func(*args, **kwargs) return res return warpper @f1 @f2 @f3 def test(): print("我是test,我没有参数") test() # 注意:@装饰器的执行顺序是从下至上 # 第一步:test = f3(test),打印第1句,返回的是f3中的warpper。 # 第二步:test = f2(test),打印第2句,此时的test是f3中的warpper,返回的是f2中的warpper # 第三步:test = f1(test),打印第3句,此时的test是f2中的warpper,返回的是f1中的warpper # 第四步,开始执行test.也就是执行f1中的warpper。打印第4句,运行func # 第五步:此时运行的func是f2中的warpper,所以打印第5句,继续运行func # 第六步:此时运行的func是f3中的warpper,所以打印第6句,继续运行func # 第七步:此时运行的func才是真正的test,打印第7句。分析结束。 # ==== 执行结果 ==== """ 执行f3装饰器了 执行f2装饰器了 执行f1装饰器了 我是f1中的warpper.我的参数是.. <function f2.<locals>.warpper at 0x00000246A34F6940> 我是f2中的warpper.我的参数是.. <function f3.<locals>.warpper at 0x00000246A34F68B0> 我是f3中的warpper.我的参数是.. <function test at 0x00000246A34F6820> 我是test,我没有参数 """
有参装饰器
有参装饰器应用场景
无参装饰器已经能够解决百分之九十的应用场景。但是有的时候还是需要有参装饰器来解决一些特定的需求,在不允许为内部函数传参的情况下就该使用到有参装饰器。它其实也是利用了闭包函数中传参的这一作用,直接再往上封装一层就好了。
基于无参装饰器,也就是解决方案三。我们再提一个需求:
下载或上传的时候如果用户是普通用户,则拒绝他通过本软件进行下载或上传,如果是VIP用户,则可以使用我们的所有功能。
解决方案一 不使用@语法糖:不使用@语法糖可以很轻松的解决,只需要为outer加上一个参数传进去即可。
import time import random # 这里懒得做登录了,用随机数代替。随机数是0则代表普通用户,随机数是1就代表vip用户。 user_genre = random.randint(0, 1) def outer(func, genre): # func 是函数的内存地址,genre是用户类型。 def warpper(*args, **kwargs): # 优化1:参数转接,warpper直接对接func,多少参数都没问题。取决于func if genre == 1: start = time.time() # 这里才是真正运行的 download 或者 upload 函数。 优化2:如果函数有了返回值,我们就可以对他进行返回 res = func(*args, **kwargs) end = time.time() if func.__name__ == "download": # func.__name__ 可以拿到函数名 print("下载{0}资源所耗费的时长为:{1:.2f}秒".format(args[0], ( end - start))) # 优化3:这里改成args[0]即可。位置传参必须一一对应,其实有个小bug,关键字传参有问题。 else: print("上传资源至{0}所耗费的时长为:{1:.2f}秒".format( args[0], (end - start))) return res # 优化2:返回func的返回值 else: print("不允许下载或上传") return warpper def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") # download函数内存地址传递进去。 变量值download接收,返回的是 warpper 函数的内存地址。此download非彼download download = outer(download, user_genre) upload = outer(upload, user_genre) # 这里直接将用户类型传递进去。(图省事,没做登录这些,求一个精简) download("www.cnblogs.com/xxx/file/x.jpg") # 由于warpper指向的函数需要一个参数url,所以直接传即可。 download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img") # ==== 执行结果 === """ 不允许下载或上传 不允许下载或上传 不允许下载或上传 """
解决方案二 使用@语法糖:由于@语法糖的限制,我们的outer
函数应当只该去接收func
,而不应该有其他的变量(实际上也接收不了其他的变量,不信你可以试试)。所以,我们需要通过闭包嵌套再给他传入一个变量。
import time import random # 这里懒得做登录了,用随机数代替。随机数是0则代表普通用户,随机数是1就代表vip用户。 user_genre = random.randint(0, 1) def outer(genre): # genre是用户类型。 def inner(func): # func 是函数的内存地址,由于改为第二层,故改名inner。 def warpper(*args, **kwargs): # 优化1:参数转接,warpper直接对接func,多少参数都没问题。取决于func if genre == 1: start = time.time() # 这里才是真正运行的 download 或者 upload 函数。 优化2:如果函数有了返回值,我们就可以对他进行返回 res = func(*args, **kwargs) end = time.time() if func.__name__ == "download": # func.__name__ 可以拿到函数名 print("下载{0}资源所耗费的时长为:{1:.2f}秒".format(args[0], ( end - start))) # 优化3:这里改成args[0]即可。位置传参必须一一对应,其实有个小bug,关键字传参有问题。 else: print("上传资源至{0}所耗费的时长为:{1:.2f}秒".format( args[0], (end - start))) return res # 优化2:返回func的返回值 else: print("不允许下载或上传") return warpper return inner @outer(user_genre) # 手动传值进去,否则运行不了。传入用户类型,如果不手动传值将会把下面被装饰函数传入进去。导致认为inner是warpper。 def download(url): """下载...""" print("正在下载{0}的资源...".format(url)) time.sleep(random.randint(1, 3)) print("下载完成...") @outer(user_genre) def upload(url): """上传...""" print("正在向{0}上传资源...".format(url)) time.sleep(random.randint(1, 3)) print("上传完成...") download("www.cnblogs.com/xxx/file/x.jpg") # 由于warpper指向的函数需要一个参数url,所以直接传即可。 download("www.cnblogs.com/yyy/file/y.jpg") upload("www.cnblogs.com/zzz/file/img") # ==== 执行结果 === """ 不允许下载或上传 不允许下载或上传 不允许下载或上传 """
有参装饰器的实现模板
# ==== 有参wraps@语法糖方式调用 ==== from functools import wraps def outer(x): def inner(func): # 装饰器名字应该取好,func代表被装饰函数。 @wraps(func) # 这里主要完全将warpper的所有文档信息等等都与func保持一致。下面会讲 def warpper(*args, **kwargs): # warpper函数对应func,也就是被执行的函数。这里做对接 # 功能扩展区,函数运行前 res = func(*args, **kwargs) # 接收func的return值 # 功能扩展区,函数运行后 return res # 将func的返回值返回出去 return warpper return inner @outer(1) # 执行outer(注意:必须手动传入参数,不然会将test传进去),拿到返回值inner.相当于@inner,再自动执行inner,将test传给形参func,返回warpper.执行test的时候实际上就是执行的wapper def test(x, y, z): print(x, y, z) return "结果" res = test(1, 2, 3) # 拿到warpper的return值 print(res)
(对于
@
语法糖调用形式来说):inner
层的参数只应该有一个func
形参来接收被装饰的函数对象,wapper
层就只该有*args
和**kwargs
,为的是和传入的func
(函数对象)做参数对接,参数不能多传递。所以就在外面套上一个outer
层函数,通过闭包传参即可,目的是做功能拓展。
扩展:wraps装饰器修饰伪装
如果不使用wraps
装饰器对warpper
函数进行装饰,那么其实偷梁换柱是有瑕疵的。我们看...
def test(x, y, z): print(x, y, z) return "结果" def outer(func): def warpper(*args, **kwargs): res = func(*args, **kwargs) return res # 将func的返回值返回出去 return warpper test = outer(test) # test变量名执行warpper内存地址。 help(test) # ==== 执行结果 ==== Ps:可以看到test的help信息还是warpper,这个偷梁换柱不彻底。 """ Help on function warpper in module __main__: warpper(*args, **kwargs) """
在使用wraps
装饰器对warpper
函数进行装饰后,那么就是真正意义上的移花接木偷天换日。继续看...
from functools import wraps def test(x, y, z): print(x, y, z) return "结果" def outer(func): @wraps(func) # 传入被装饰对象。完成移花接木 def warpper(*args, **kwargs): res = func(*args, **kwargs) return res # 将func的返回值返回出去 return warpper test = outer(test) # test变量名执行warpper内存地址。 help(test) # ==== 执行结果 ==== Ps:移花接木成功 """ Help on function test in module __main__: test(x, y, z) """