当前位置: 首页 > 后端技术 > Python

Python装饰器_0

时间:2023-03-25 23:39:32 Python

Python中万物皆对象,函数也是对象。一个函数可以赋值给一个变量,一个函数可以作为参数传递给另一个函数,一个函数可以通过return语句返回一个函数。装饰器是一个接受一个函数并返回一个函数的函数。这乍一听可能有点复杂,但装饰器本质上是一个函数。既然要学习装饰器,那首先得知道它是用来做什么场景的。装饰器通过面向切面的编程来增强代码的健壮性,例如:日志记录、处理缓存、权限验证等。接下来我们将一步步学习Python中装饰器的用法。先看一个简单的函数定义,函数只有一个函数,printHelloWorld:defhello():print('HelloWorld!')现在一个新的需求来了,在原函数执行之前添加日志功能,于是就有了下面的代码:defhello():print('runhello')print('HelloWorld!')现在上面的问题就解决了,只需要加一行代码就搞定了。但问题是,在实际工作场景中,我们可能需要修改的不仅仅是一个hello函数,而是10个、20个需要同时添加日志功能的函数。这时候问题就来了,我们不太可能一个函数一个函数地复制这行代码,而那个时候,有可能加的不仅仅是一行代码,有可能是几百行。而这样会造成很多重复的代码。当代码重复过多时,就得小心了,很容易造成意想不到的bug,排查和维护也很困难。一个容易想到的方法是定义一个函数log,专门打印日志,然后在每个函数中调用log函数:deflog():print('runhello')defhello():log()print('HelloWorld!')仍然需要修改hello函数内部的代码。不是说不能做,而是明显违背了开闭原则——对实现的功能代码关闭,对扩展开放。虽然这个短语经常用于面向对象的编程思想,但它同样适用于函数式编程。我们可以考虑使用高阶函数来解决这个问题,或者定义一个日志函数,但是这次它接收一个函数作为参数,而这个函数首先执行打印日志的函数,并在日志函数结束:deflog(func):print('runhello')func()defhello():print('HelloWorld!')log(hello)上面的代码利用了一个函数可以作为参数传递给另一个函数特征,解决了需要修改原函数内部代码的问题。虽然这是功能上的实现,并没有破坏原函数的内部逻辑,但是破坏了函数调用者的代码逻辑。也就是说,原代码中所有调用hello函数的语句都要从hello()改成log(hello),这样看起来比较麻烦。简单的装饰器那么,现在是介绍装饰器概念的时候了,装饰器非常擅长以Pythonic的方式解决这类问题。让我们看一下编写装饰器的最简单方法:deflog(func):defwrapper():print('runhello')func()returnwrapperdefhello():print('HelloWorld!')hello=log(hello)hello()这段代码充分体现了上面介绍的函数的特点。一个函数可以赋值给一个变量,一个函数可以作为参数传递给另一个函数,一个函数可以通过return语句返回一个函数。现在日志函数是一个装饰器。首先定义一个日志函数,它接收一个函数作为参数,并在其内部定义一个包装函数。wrapper函数打印日志后,调用传入的func函数(也就是hello函数),在log函数的最后返回这个内部定义的函数。在示例代码的底部,我们将hello函数作为参数传递给log函数,并将返回结果赋值给变量hello。此时hello变量指向的是日志装饰,而不是原来的hello函数。包装器返回的内部函数包装器。现在调用者不需要修改调用方法,仍然使用hello()来调用hello函数,但是增强了它的功能,会在执行print('HelloWorld!')逻辑。在上面的代码中,我们已经在功能上实现了装饰器的效果。但实际上,Python直接在句法层面支持装饰器模式。只需要一个@符号就可以让上面的代码更易读,更容易维护。deflog(func):defwrapper():print('runhello')func()returnwrapper@logdefhello():print('HelloWorld!')hello()@符号是Python在语法层面提供的语法糖,但它本质上等同于hello=log(hello)。以上就是最精简的Pythonic装饰器。不管你以后遇到多么复杂的装饰器,请记住,它的最终本质其实是一个函数,只是它利用了Python中的一些函数特性,使其能够处理更复杂的业务场景。装饰函数有参数和返回值。装饰师的实际工作场景。我们写的函数往往非常复杂。如果我们想写一个更通用的装饰器,我们需要做一些细节工作。不过,你已经了解了装饰器的本质,剩下的例子也不是很难理解。你只需要在特定场景下使用特定功能的装饰器即可。deflog(func):defwrapper(*args,**kwargs):print('runhello')returnfunc(*args,**kwargs)returnwrapper@logdefhello(name):print('HelloWorld!')returnf'Iam{name}.'result=hello('xiaoming')print(result)*args,**kwargs这两个变长参数很好的解决了装饰器的普适性问题,使得装饰器装饰的时候任何函数,参数都可以按原样传递给原始函数。最后在wrapper函数调用func函数前加了一条return语句,其作用是将原函数的返回结果返回给调用者。保留装饰函数的元信息的装饰器日志。装饰器内的包装函数打印日志。代码print('runhello')是一个固定的字符串。如果我们想让它根据函数名自动改变打印结果,比如print(f'run{functionname}.')这种形式。每个函数都有一个__name__属性,可以返回它的函数名:defhello(name):print('HelloWorld!')print(hello.__name__)#hello但问题是,使用日志装饰器后,原来的hellofunction已经指向了wrapper函数,所以你测试一下,你会发现被修饰的hello函数的__name__属性变成了wrapper,这显然不是我们想要的结果。我们可以用wrapper.__name__=func.__name__行语句来解决这个问题,但是我们有更好的方法。Python有一个内置的装饰器functools.wraps可以帮助我们解决这个问题。从functoolsimportwrapsdeflog(func):@wraps(func)defwrapper(*args,**kwargs):print(f'run{func.__name__}')returnfunc(*args,**kwargs)returnwrapper@logdefhello(name):print('HelloWorld!')returnf'Iam{name}.'print(hello('xiaoming'))print(hello.__name__)装饰器本身有参数也许你想控制log装饰器的日志级别,所以给装饰器传递参数很容易想到。我们来看一个需要接收参数的装饰器的例子:fromfunctoolsimportwrapsdeflog(level):defdecorator(func):@wraps(func)defwrapper(*args,**kwargs):iflevel=='warn':print(f'run{func.__name__}')eliflevel=='info':passreturnfunc(*args,**kwargs)returnwrapperreturndecorator@log('warn')defhello(name):print('HelloWorld!')returnf'Iam{name}.'result=hello('xiaoming')print(result)带参数的装饰器相比之前的装饰器多了一层功能嵌套。实际效果是hello=log('warn')(hello),首先调用log('warn')返回的是一个内部装饰器函数,然后就相当于hello=decorator(hello),其实,这一步是一样的没有参数的装饰器也是一样的。装饰器支持有参和无参。有时候你可能会遇到更变态的需求。您需要使用带或不带参数的装饰器。有很多解决方案。我会在这里给它。想出一个比较简单易懂的实现。从functoolsimportwrapsdeflog(level):ifcallable(level):@wraps(level)defwrapper1(*args,**kwargs):print(f'run{level.__name__}')returnlevel(*args,**kwargs)returnwrapper1else:defdecorator(func):@wraps(func)defwrapper2(*args,**kwargs):iflevel=='warn':print(f'run{func.__name__}')eliflevel=='info':passreturnfunc(*args,**kwargs)returnwrapper2returndecorator@log('warn')defhello(name):print('HelloWorld!')returnf'我是{name}.'@logdefworld():print('world')print(hello('xiaoming'))world()callable可以判断传入的参数是否可调用,但是需要注意的是callable只支持Python3.2及以上版本,具体可以查看官方文档。类装饰器比函数装饰器更灵活、更强大。可以在Python类中定义一个__call__方法,这样它就可以在不实例化本身的情况下被调用,此时__call__中的代码将被执行。类Log(对象):def__init__(self,func):self._func=funcdef__call__(self):print('before')self._func()print('after')@Logdefhello():print('helloworld!')hello()装饰器装饰顺序一个函数其实可以同时被多个装饰器装饰,那么多个装饰器的装饰顺序是怎样的呢?让我们在下面探讨它。defa(func):defwrapper():print('abefore')func()print('aafter')returnwrapperdefb(func):defwrapper():print('bbefore')func()print('bafter')returnwrapperdefc(func):defwrapper():print('cbefore')func()print('cafter')returnwrapper@a@b@cdefhello():print('HelloWorld!')hello()上面代码的结果:abeforebbeforcbeforeHelloWorld!cafterbafteraafter多重修饰语法等价于hello=a(b(c(hello)))。根据打印出来的结果,不难发现这段代码的执行顺序。如果你了解过Node.js的Koa2框架的中间件机制,那么你一定很熟悉上面代码的执行顺序。事实上,Python装饰器也遵循洋葱模型。多个装饰器的代码执行顺序就像剥洋葱一样,先从外进入到里面,再从内到外进入。给大家留个思考题:最后的hello.__name__指向装饰器内部的哪个wrapper函数?装饰器实战理解了装饰器之后,我们就要使用它们了。文章开头提到了装饰器的使用。我们来看一个在实际场景中使用装饰器的例子。Flask是PythonWeb生态中非常流行的微框架,你可以在GitHub上查看它的源代码。下面是一个用Flask编写的最小Web应用程序。这里@app.route("/")装饰器的作用是绑定发送到处理函数hello的根路由/请求进行处理。这样,当我们启动FlaskWebServer时,在浏览器地址访问http://127.0.0.1:5000/就可以得到返回结果Hello,World!。fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defhello():return"Hello,World!"当然,更多的装饰器使用场景还是需要大家自己去探索和发现。起始地址:https://jianghushinian.cn/