原文链接

来简单聊聊python的装饰器呀~​mp.weixin.qq.com

导语

之前很多小伙伴留言给我说看别人写的代码经常会感觉云里雾里的,完全看不懂,其实那些代码无非就是用了些python语法中的特殊"技巧"罢了,而你对这些内容又不太熟悉,所以才会感觉很难读懂那些大佬们的代码。所以今天,我们就先来聊聊其中最常见的一种,即python的装饰器,以后有时间,我们可以再接着聊聊python的其他一些"骚操作"。废话不多说,让我们愉快地开始吧~

装饰器

装饰器是个啥?

要了解装饰器,自然要先明白它到底是个啥东西啦。简单来说,装饰器其实就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。

举个例子?

让我们来举个例子以更好地理解上面那句话的含义。比方说我们想在某个函数执行前都打印一次pikachu字符,那么我们可以这样做:

def logo(func):def wrapper(*args):print('''
_____ _ _              _
|  __ (_) |            | |
| |__) || | ____ _  ___| |__  _   _
|  ___/ | |/ / _` |/ __| '_ | | | |
| |   | |   < (_| | (__| | | | |_| |
|_|   |_|_|___,_|___|_| |_|__,_|''')return func(*args)return wrapperdef sub(a, b):return a - bdef multiply(a, b):return a * bsub_wrapper = logo(sub)
multiply_wrapper = logo(multiply)
print(sub_wrapper(2, 5))
print(multiply_wrapper(2, 5))

运行上面的代码后的效果如下:

使用python提供的装饰器语法,则上面的代码可以写成:

def logo(func):def wrapper(*args):print('''
_____ _ _              _
|  __ (_) |            | |
| |__) || | ____ _  ___| |__  _   _
|  ___/ | |/ / _` |/ __| '_ | | | |
| |   | |   < (_| | (__| | | | |_| |
|_|   |_|_|___,_|___|_| |_|__,_|''')return func(*args)return wrapper@logo
def sub(a, b):return a - b@logo
def multiply(a, b):return a * bprint(sub(2, 5))
print(multiply(71, 5))

运行上面的代码后得到的效果如下(即和第一版代码的效果是一样的):

通过这个例子,相信聪明的小伙伴们已经对装饰器有了一个大概的认识了。即装饰器其实就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。

为什么要使用装饰器?

其实上面的例子已经隐含了这个问题的答案。即当我们想为很多不同的函数添加相同的功能时(例如计时、保存日志等等),我们可以利用装饰器来使得我们的代码更加整洁(或者说pythonic?)

当然,如果你认为装饰器的用法仅限于此,那你就大错特错了。只要脑回路足够多,我们就可以利用python的装饰器创造"无限的"可能。这里举个简单的例子吧:

def fib(n):if n <= 1: return 1return fib(n-1) + fib(n-2)

上面的函数很简单,功能就是计算斐波那契数列,我们可以看一下它的运行时间是多久:

看起来好慢的样子?这是因为上面这段代码存在一个问题,即你想要计算fib(30),那你就需要计算fib(28)和fib(29),而计算fib(29)的时候,你还得再算一遍fib(27)和fib(28),显然,这里fib(28)被重复计算了一次。以此类推,上面这段代码中是存在很多重复计算的。现在,我们尝试借助装饰器来解决这个问题(即利用装饰器来存储运算的中间结果以避免重复运算):

def memory(func):cache = {}def wrapper(*args):if args not in cache:cache[args] = func(*args)return cache[args]return wrapper@memory
def fib(n):if n <= 1: return 1return fib(n-1) + fib(n-2)

现在,我们再来查看一下它的运行时间:

我们可以发现运行时间直接从微秒级别下降到了纳秒级别(我没有在暗示光刻机

)。

类装饰器

看到"类装饰器"这个词,很多小伙伴可能会发问了:"你前面不是说装饰器其实就是个函数吗?咋又成类了?"害,上面这么说是为了方便大家快速理解装饰器这个概念嘛,不然一股脑地抛出所有东西,很容易让人丈二和尚摸不着头脑哒~

现在,我们来纠正一下前面的说法,即并非只有函数对象可以作为装饰器使用,事实上,在python中,某个对象能否作为装饰器形式使用,只有一个要求,即该对象必须是一个"可被调用(callable)的对象"。显然,函数肯定是可被调用的。而对于类来说,我们则可以通过定义类的__call__这个魔法方法,来使得它可调用。举个例子:

import functoolsclass MemoryClass():def __init__(self, is_logging, func):self.is_logging = is_loggingself.func = funcif self.is_logging:print('欢迎关注微信公众号: Charles的皮卡丘')self.cache = {}def __call__(self, *args):if args not in self.cache:self.cache[args] = self.func(*args)return self.cache[args]def memory(is_logging):return functools.partial(MemoryClass, is_logging)@memory(is_logging=True)
def fib(n):if n <= 1: return 1return fib(n-1) + fib(n-2)print(fib(50))

运行结果如下:

显然,相比函数装饰器,类装饰器具有更加灵活,封装性更好等优点~

使用装饰器会带来哪些问题?

显然,根据辩证法,所有事物都是有利有弊的。那么我们使用装饰器的过程中可能会存在哪些问题呢?我们又该如何解决这些问题呢?别急,我们慢慢说~

(1) 函数的属性会发生变化

如前所述,装饰器用新函数来替换了原来的旧函数。那么显然,这个新函数就会缺少很多旧函数的属性,举个例子:

def advertising(func):def wrapper(*args):print('欢迎关注微信公众号: Charles的皮卡丘')return func(*args)return wrapper@advertising
def add_1(a, b):'''加法运算'''return a + bdef add_2(a, b):'''加法运算'''return a + bprint('add_1文档: ', add_1.__doc__)
print('add_1函数名: ', add_1.__name__)
print('add_2文档: ', add_2.__doc__)
print('add_2函数名: ', add_2.__name__)

上面代码的运行结果如下:

显然,我们可以发现,使用了装饰器之后,我们无法正确地获取函数原有的文档和名字了。该问题的解决方案如下:

import functoolsdef advertising(func):@functools.wraps(func)def wrapper(*args):print('欢迎关注微信公众号: Charles的皮卡丘')return func(*args)return wrapper@advertising
def add_1(a, b):'''加法运算'''return a + bprint('add_1文档: ', add_1.__doc__)
print('add_1函数名: ', add_1.__name__)

重新运行代码,获得的结果如下:

完美解决~

(2) 函数参数的获取

先来看一段代码(这里我们想利用装饰器来保证除法运算的除数不是一个接近零的数):

import functoolsdef check(func):@functools.wraps(func)def wrapper(*args, **kwargs):if kwargs.get('divisor') <= 1e-6 and kwargs.get('divisor') >= -1e-6:raise ValueError('除数不能为0!')return func(*args)return wrapper@check
def division(dividend, divisor):return dividend / divisorprint(division(5, 1))

运行之后发现报错:

为什么呢?这是因为我们传入的除数(divisor)是一个位置参数,而我们却用关键字参数来获取它了。该问题的解决方案如下:

import inspect
import functoolsdef check(func):@functools.wraps(func)def wrapper(*args, **kwargs):getcallargs = inspect.getcallargs(func, *args, **kwargs)if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:raise ValueError('除数不能为0!')return func(*args)return wrapper@check
def division(dividend, divisor):return dividend / divisor

现在我们就可以正常运行上面的代码啦:

(3) 修改外层变量

假设我们想看下函数被调用的次数,那么我们也许会把代码写成这个样子:

import inspect
import functoolsdef check(func):count = 0@functools.wraps(func)def wrapper(*args, **kwargs):count += 1print(f'第{count}次调用')getcallargs = inspect.getcallargs(func, *args, **kwargs)if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:raise ValueError('除数不能为0!')return func(*args)return wrapper@check
def division(dividend, divisor):return dividend / divisorprint(division(5, 1))

但是上面这个代码运行时会报错:

出错的原因是当解释器执行到count += 1时,并不知道count是一个在外层作用域定义的变量,它把count当成局部变量在当前作用域内进行查找了。最终因为没找到count变量相关的任何定义而抛出错误。为了解决这个问题,我们可以这样写:

import inspect
import functoolsdef check(func):count = 0@functools.wraps(func)def wrapper(*args, **kwargs):nonlocal countcount += 1print(f'第{count}次调用')getcallargs = inspect.getcallargs(func, *args, **kwargs)if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:raise ValueError('除数不能为0!')return func(*args)return wrapper@check
def division(dividend, divisor):return dividend / divisorprint(division(5, 1))

即通过nonlocal关键字来告诉解释器count变量并不属于当前作用域,可以到外面找找这个变量的定义。由此,我们的代码就可以正常运行啦:

使用多个装饰器

就像很多人发自拍一样,必须化妆品和美颜相机一起用才能体现自己的颜值(好像必须声明一下,号主是男生!!!),那么我们该如何使用多个装饰器去"装饰"某个函数呢?

很简单,我们只需要这样做:

def DecoratorA(func):def wrapper(*args):print('先化妆')return func(*args)return wrapperdef DecoratorB(func):def wrapper(*args):print('再美颜')return func(*args)return wrapper@DecoratorA
@DecoratorB
def add(a, b):return a + bprint(add(1314, 2020))

运行效果如下:

总结

因为只是简单聊聊,纯属抛砖引玉,所以很多细节我都没有去说(虽然有些其实已经体现在样例代码里了)。希望小伙伴们可以通过阅读本文大致了解一下python中的装饰器到底是个啥玩意,至于如何更好地利用好这个语法糖,还得靠小伙伴们自己多多练习,或者阅读一些优秀的开源代码才行呀~毕竟修行说到底还是得靠个人嘛~

更多

喜欢文章的小伙伴记得帮我点点好看/赞/分享呀~

参考文献:

[1]. https://www.zhihu.com/question/26930016

python 打印皮卡丘_来简单聊聊python的装饰器呀~相关推荐

  1. python 打印皮卡丘_用python打印你的宠物小精灵吧

    我们来通过一个有趣的例子开始编写我们的第一个python代码. 本文涉及的python基础语法为:print输出函数,赋值,字符串 print() print()是python的一个内置函数,用于打印 ...

  2. python 打印皮卡丘_Python到底是什么?学姐靠它拿了5个offer

    你ZAO吗? 最近陌陌发布了一款很有意思的产品--ZAO,这款AI换脸的产品刷爆朋友圈! 这款产品火爆到什么程度呢? 正在使用ZAO的用户会发现,想要生成一段新的AI换脸视频,已经不是等待几秒.排队第 ...

  3. python的皮卡丘如何写代码,用python画皮卡丘的代码

    python皮卡丘编程代码 import turtledef getPosition(x, y): (x) (y) print(x, y)class Pikachu: def __init__(sel ...

  4. python打印皮卡丘步骤_编程作战丨如何利用python绘制可爱皮卡丘?

    好莱坞真人电影<精灵宝可梦:大侦探皮卡丘>预告片已经发布了,正片将于今年5月10日上映. 如果要做一个「童年梦想排行榜」的话,相信「拥有一只皮卡丘」这个梦想一定会名列前茅! 毕竟,谁不想揉 ...

  5. python 打印皮卡丘_Python干掉了97%的办公软件?

    "21世纪,不会Python等于文盲." 这句流行语并非夸张,<2020年职场学习趋势报告>显示,在2020年最受欢迎的技能排行榜,Python排在第一. 除职场外,P ...

  6. 怎么用python画皮卡丘_实现童年宝可梦,教你用Python画一只属于自己的皮卡丘

    原标题:实现童年宝可梦,教你用Python画一只属于自己的皮卡丘 大数据文摘出品 作者:李雷.蒋宝尚 还记得小时候疯狂收集和交换神奇宝贝卡片的经历吗? 还记得和小伙伴拿着精灵球,一起召唤小精灵的中二模 ...

  7. python画图皮卡丘_用python画一只可爱的皮卡丘

    #!/usr/bin/env python # -*- coding:utf-8 -*- from turtle import * ''' 绘制皮卡丘头部 ''' def face(x,y): &qu ...

  8. 用python实现视频换脸_超简单使用Python换脸实例

    换脸! 这段时间,deepfakes搞得火热,比方说把<射雕英雄传>里的朱茵换成了杨幂,看下面的图!毫无违和感! 其实早在之前,基于AI换脸的技术就得到了应用,比方说<速度与激情7& ...

  9. python关键字中文意思_中英文简单介绍Python关键字 -- Python Key Words

    直奔主题,理解Python关键字有利于正确理解Python中的命令,对于系统掌握Python语法有着十分重要的作用. 1, False : Boolean Value as no 2, True : ...

最新文章

  1. WPF-002 下拉列表的简单实现
  2. html5中的dom中的各种节点的层次关系是怎样的
  3. 服务器怎么导出数据库文件,怎么导出服务器数据库文件
  4. killall命令_没想到Linux命令也有“吓人”的一面……
  5. 电改:国内电网的账单也与时俱进了
  6. java 定时器qz xm配置_java_Java的作业调度类库Quartz基本使用指南,一、常用接口: 1、Job接口: - phpStudy...
  7. 【机器学习】基于AutoEncoder的BP神经网络的tensorflow实现
  8. Photoshop 2020免注册登录版,自用下载安装教程
  9. 【ASP.NET】RSA加密,前端加密,后端解密,有效哦!
  10. c语言运用(1)口算比赛
  11. 前端学习2-JavaScript
  12. centos8安装失败 Linux dd,在RHEL 8/CentOS 8上安装Telegraf的方法
  13. 车牌识别关键技术-车牌定位
  14. Macbook pro外接显卡实现深度学习
  15. c、c++的getchar()函数
  16. php json数据值,php操作JSON格式数据
  17. appJSON[tabBar][borderStyle] 字段需为 black 或 white console.error @ VM1402:1 (anonymous) @ VM1415:2
  18. linux执行sh脚本报错的解决办法
  19. 地震发生时,我们如何避震自救?
  20. 低潮是人生最佳升值期

热门文章

  1. WCF分布式开发常见错误(3):客户端调用服务出错
  2. 第一章 什么是数组名?
  3. LaTeX tikz初探——利用emoji画GPS卫星3D分布图(3)
  4. 用c语言实现数据结构算法将两个有序链表并为一个有序链表的算法,,(完整版)数据结构-习题集答案-(C语言版严蔚敏)...
  5. linux在当前目录下打开终端,linux - 终端:在窗口中打开当前路径? - Ubuntu问答...
  6. 文件上传功能如何测试
  7. mongodb mysql配置_mongoDB数据库原生配置
  8. 批量画同心不同半径圆lisp_【微课视频】青岛版数学六年级上册5.1圆的认识
  9. 用户的大量数据保存在计算机的,计算机基础理论复习题
  10. c语言 mysql 查询数字_c语言mysql查询数据库