
装饰器在编程的世界里面, 随着项目规模一步一步变大, 我们总是会遇到一个大家都要面对的挑战, 就是到底要怎么才能在不改动核心功能代码的情况下, 给好几个函数都加上一样的一些什么附加行为呢?这个做法其实很像是在一本书里面的好几段话的上面和下面都加上统一格式的页眉以及页脚内容。要是咱们一个个地去执行复制和粘贴操作的话, 不但工作效率特别低, 而且如果某一天这些内容需要修改了, 那么你就不得不去手动地把每一个地方都更新一遍。这种做法想一想就觉得简直就是一场让人头疼的大梦魇。装饰器就是用来解决这个痛点的, 它是一种优雅的工具, 用起来非常巧妙, 它能让代码的复用过程变得十分轻松、十分自在, 同时, 它还能让函数的核心逻辑不再被那些复杂而且多余的额外代码给淹没掉。这篇文章, 是要带着大家伙儿一块儿把这装饰器的核心概念给它弄明白, 同时还要把人家工作的那些个原理也给剖析透了, 并且还会告诉你一些最佳的实践法子是什么样子, 这最终的目的, 就是想让那些刚入门的初学者们能够轻轻松松就把这个特别强大的工具给掌握住了, 这样一来, 你写出来的代码就能变得更优雅了, 也更容易去维护了。一、为何需要装饰器揭示代码复用的痛点你就去想象这个场景, 说你正在着手开发一个web应用程序, 并且里面有好几个函数, 都需要去实现下面所提到的那些功能。假如大家使用老旧的手法, 那么就很有可能需要在每一个单独的函数里面, 去重复地写下这一段又一段相同的逻辑代码。表面上看起来这样做挺简单的, 但是实际上却会导致两样非常严重的问题出现。代码存在冗余问题的表现是, 相同的逻辑被一次又一次地复制粘贴到了很多地方去, 这种做法不仅让代码的整体体积变大了, 还提高了引入错误这种bug的風險。一旦以后需要去修改某个功能, 比方说要改掉日志记录的格式, 你就必须把所有相关的函数都找出来, 然后一个一个地去进行修改, 这毫无疑问是一件非常耗费时间而且很容易出错的事情, 关于关注点混淆这一点来讲, 函数的核心任务被一些额外的逻辑给包围起来了。比如说, 有一个用来计算订单里面所有的钱总数的那个函数, 你仔细去看它写的代码, 就会发现, 里面到处都是做身份验证的代码, 还有写日志的代码。这种情况导致大家看这个代码的时候觉得非常难读, 同时呢, 也把这个函数最开始应该负责的主要事情给遮掩住了, 让人看不清它到底该干什么。装饰器这个东西, 它是为了解决那些问题, 而专门被设计出来的。因为它允许你不去修改函数源代码, 就给它添加上额外的功能。这就实现了关注点的分离。同时也实现了代码的高度复用。二、理解装饰器的核心一个简单的例子那个地方, 装饰器这个东西, 说白了就是一个函数, 它把另一个函数拿来做输入, 然后, 返回来一个新函数, 这个新函数是经过所谓装饰处理的, 所以说这个新函数里头, 不光有原本那个函数的内容, 还有咱们想要加进去的那些额外的行为。为了便于大家搞明白这一件事情, 咱们可以拿一个十分简便的案例来把装饰器的运作机理掰开揉碎了讲一讲。假设现在存在一个名为greet()的函数, 这个函数的任务就是把“Hello, world!”这句话给打印出来。眼下我们的想法是, 不用去改动greet()函数里面的代码内容, 仅仅是在该函数即将被执行的时候以及在执行完毕之后, 分别另外输出一段文字内容, 这就好比是给原来的东西套上了一层壳子, 给它进行了包装处理似的。原始函数def greet():print(Hello, world!)手动实现装饰器效果我们是可以去定义一个被标识为空的装饰器函数的, 这个函数它是会去接收一个名为func的函数的, 然后是在它的内部再去定义一个()函数的, 这个()函数它是要负责去的执行一些额外的逻辑的, 同时还是要在其中去调用一下原始的func这个函数的。# 装饰器函数def add_extra(func):# 包装函数def wrapper():print(Starting the greetings...) # 原始函数执行前的代码func() # 调用原始函数print(Greetings complete!) # 原始函数执行后的代码return wrapper # 返回包装函数# 手动包装 greet() 函数enhanced_greet add_extra(greet)# 调用被装饰后的函数enhanced_greet()输出结果Starting the greetings...Hello, world!Greetings complete!在这个例子当中, 那个()函数会接受greet()作为参数使用, 然后返回一个名字叫作()的新函数给调用者, 这个时候变量现在指向的就是这个新产生的名为()的函数内容, 当我们去调用那个()的时候, 实际上被执行的是在()函数内部所编写的代码逻辑程序, 通过这种方式达到了这样的效果, 也就是说它在greet()函数被调用的过程中, 实现了在开头和结尾分别添加额外逻辑操作的处理过程。三、使用语法糖让装饰器更优雅虽然手动包装的方式确实能帮助大家搞清楚装饰器是怎么回事, 但在实际开发的过程里头, 提供了一种看起来更加优雅也更为简洁的办法来用到装饰器这个概念上, 而这个办法呢, 其实就是使用at这个符号, 大家通常也把这个东西叫做语法糖。在利用这种语法机制的情况下, 大家可以把之前提到的那一个案例, 进行重新编写和整理表达为这样的形式:# 装饰器函数def add_extra(func):def wrapper():print(Starting the greetings...)func()print(Greetings complete!)return wrapper# 使用 语法应用装饰器add_extradef greet():print(Hello, world!)# 调用被装饰后的函数greet()输出结果Starting the greetings...Hello, world!Greetings complete!这里的, 也就是等价于我们之前手动实现的greet (greet)。它在函数定义的时候, 就自动完成了装饰器函数的调用, 并且把结果替换进去。这样的话, 代码就变得更加清晰, 也更加容易读。这种语法呢, 是在实际项目当中被广泛使用的主要原因。四、装饰器背后的“陷阱”元数据丢失问题在大家因为使用了装饰器而感到非常方便的这个情况之下, 也存在着一个必须被提到而且需要引起注意的问题内容, 这个问题的具体表现就是会导致原本存在的元数据发生丢失现象。当我们在使用装饰器的时候, 那个返回的小括号里面的函数, 它会取代掉原来的函数位置。这样的话, 原始函数所具备的元数据信息, 比方说它的属性里面记录的那个名为函数名的部分, 以及文档字符串的这个属性内容, 全部都会因为替换行为, 而变成用现在这个小括号里面的新函数的元数据结果来覆盖了。举个例子, 当你在不借助任何额外工具的前提下去查看已经被装饰过的 greet 函数里面包含的属性时, 你就会发现该属性的名称不再保持为“greet”了, 而是变成了“”。print(greet.__name__) # 输出: wrapper在那些简单的程序中, 这个情况可能不会造成太大的影响。但在大型项目中, 或者是在进行库开发的工作时, 元数据对于调试工作是至关重要的。对于文档生成工作来说, 它也是很重要的。对于自动化测试工作来说, 它也是必不可少的。一旦元数据丢失了, 就会给这些工作带来非常大的不便之处。五、解决方案.wraps登场为了解决元数据丢失的问题, 标准库里面的模块提供了一个叫作wraps()的装饰器。它能够让被装饰函数的元数据, 比如文档字符串这些内容, 自动地被复制到包装函数的上面去, 因此能够把这个问题给非常完美地解决掉。在把.wraps()这个方法给加上以后, 那个装饰器具体的代码长下面的这个样子。import functoolsdef add_extra(func):functools.wraps(func) # 保留原始函数的元数据def wrapper():print(Starting the greetings...)func()print(Greetings complete!)return wrapperadd_extradef greet():这是一个打招呼的函数print(Hello, world!)# 检查被装饰后的函数名print(greet.__name__) # 输出: greet在这个例子当中, .wraps(func)这个东西, 它是确保了()这个函数去继承了greet()函数的元数据, 这么一来, 就让greet的输出结果是恢复到了greet。所以, 当你那个啥, 在写一个装饰器的时候, 一定要记得要用那个 .wraps 这个东西, 去把你的函数包装一下, 这个, 被看作是 里面公认的一个好的实践方式咧。为啥子要这样做?因为这个做法, 一方面能够保证说你写的代码, 不管是在哪种什么不同的场景下面去运行的时候, 都能顺顺利利地正常工作而另一方面, 它同时也体现出了你这个人对待代码质量的那种非常严谨、非常认真的一种态度, 就是说你对你的代码是很上心的。六、总结与展望装饰器的广阔应用前景通过阅读并仔细分析这篇文章的内容, 相信大家已经对 装饰器这个概念形成了非常完整的认识, 我们从那些需要用装饰去解决实际问题的具体场景出发, 一步一步地讲解了它到底是怎么运作的, 还用了一个特别简单的例子来说明它的功能有多强, 往后我们又把 提供的那个 符号的语法糖也给讲透了, 这样子在调用的时候就会方便很多到了最后的地方, 我们也讨论了一下在使用的时候可能出现的元数据丢失的状况, 并且给出了一个标准的解决办法, 那就是使用 .wraps()这个方法。现在, 你已经是掌握了装饰器的基本的那些原理的以及那些实践的技巧的那种状态的一个实现了。但是这也就仅仅只是是一个冰山的一的那个角度的一个存在而已呢。因为装饰器它不仅仅是可以仅仅地只用于那个函数的一种之上的, 而且它还可以仅仅地只用于类的一种之上的。然后甚至, 它还可以接受参数的一种情况的。那么在未来的那些学习之中的时候, 你可以进一步去探索那些装饰器的更多的属于高级用法的方面的一些内容。例如一些具体的情况如下所示:一旦你学会了使用装饰器, 这就好比是手里攥着一把属于编程领域的瑞士军刀一样。通过这种方式, 你的代码就能够变得更加简洁起来, 同时也具备着很高的效率, 并且后续维护起来也相对容易一些。我们期望这篇长文能够起到一个标志性的作用, 从而成为你在进行学习过程当中的一个重要阶段节点, 在此祝愿你能够在编程的世界里面过得十分愉快。#基础#