来源:麦叔编程
作者:麦叔
之前发过两篇装饰器的文章,当时为了博取眼球,标题取得虚头巴脑。现在把两篇合并在一起,重新发在『Python终结者』系列中,方便大家学习。
前不久,我面试过一个要求月薪30k+的程序员,还有一个浙大毕业的新人,两个人都自称最熟悉的编程语言是Python,但没有一个人知道装饰器。看完这篇文章,至少你在这方面超越了他们。
看透这篇文章,你至少超过了80%的Python学习者。
就算一周学透一个重要知识点,不久之后,你就会成为很厉害的存在!加油!!
对于Python学习者,一旦过了入门阶段,你几乎一定会用到Python的装饰器。
它经常使用在很多地方,比如Web开发,日志处理,性能搜集,权限控制等。
还有一个极其重要的地方,那就是面试的时候。对,装饰器是面试中最常见的问题之一!
抛出问题
看这段代码:
def step1(): print('step1.......') def step2(): print('step2......') def step3(): print('step3......')
step1()
step2()
step3()
代码中定义了3个函数,然后分别调用这3个函数。假设,我们发现代码运行很慢,我们想知道每个函数运行分别花了多少时间。
我们可以在每个函数中添加计时的代码:
下面的例子只在step1中添加了相关代码作为示例,你可以自行给step2和step3添加相关代码。
import time def step1(): start = time.time()
print('step1.......')
end = time.time()
used = end - start
print(used) def step2(): print('step2......') def step3(): print('step3......')
step1()
step2()
step3()
这个方法可行!但用你的脚指头想想也会觉得,这个方法很繁琐,很笨拙,很危险!
这里只有3个函数,如果有30个函数,那不是要死人啦。万一修改的时候不小心,把原来的函数给改坏了,面子都丢光了,就要被人BS了!
一定有一个更好的解决方法!
更好的解决方法是使用装饰器。
装饰器并没有什么高深的语法,它就是一个实现了给现有函数添加装饰功能的函数,仅此而已!
import time def timer(func): '''统计函数运行时间的装饰器''' def wrapper(): start = time.time()
func()
end = time.time()
used = end - start
print(f'{func.__name__} used {used}') return wrapper def step1(): print('step1.......') def step2(): print('step2......') def step3(): print('step3......')
timed_step1 = timer(step1)
timed_step2 = timer(step2)
timed_step3 = timer(step3)
timed_step1()
timed_step2()
timed_step3()
上面的timer函数就是个装饰器。
简单说就是把原来的函数给包了起来,在不改变原函数代码的情况下,在外面起到了装饰作用,这就是传说中的装饰器。它其实就是个普通的函数。
如果你觉得有点懵逼,需要加强一些对Python函数的理解。函数:
可以作为参数传递
可以作为返回值
也可以定义在函数内部
然后,我们不再直接调用step1, 而是:
timed_step1 = timer(step1) timed_step1()
简洁点,也可以这样写:
timer(step1)() timer(step2)() timer(step3)()
这样可以在不修改原有函数代码的情况下,给函数添加了装饰性的新功能。
但是仍然需要修改调用函数的地方,看起来还不够简洁。有没有更好的办法呢?当然是有的!
我们可以在被装饰的函数前使用@符号指定装饰器。这样就不用修改调用的地方了,这个世界清净了。下面的代码和上一段代码功能一样。在运行程序的时候,Python解释器会根据@标注自动生成装饰器函数,并调用装饰器函数。
import time def timer(func): '''统计函数运行时间的装饰器''' def wrapper(): start = time.time()
func()
end = time.time()
used = end - start
print(f'{func.__name__} used {used}') return wrapper @timer def step1(): print('step1.......') @timer def step2(): print('step2......') @timer def step3(): print('step3......')
step1()
step2()
step3()
到了这里,装饰器的核心概念就讲完了。
剩下的基本都是在不同场合下的应用。如果你是大忙人,不想学的太深,可以搜藏本文章,以后再回来看。
但是记得点在看。据说点了得都变帅了,也找到了好工作,或者升职加薪了。
上面是一个最简单的例子,被装饰的函数既没有参数,也没有返回值。下面来看有参数和返回值的情况。
我们把step1修改一下,传入一个参数,表示要走几步。
import time def timer(func): '''统计函数运行时间的装饰器''' def wrapper(): start = time.time()
func()
end = time.time()
used = end - start
print(f'{func.__name__} used {used}') return wrapper @timer def step1(num): print(f'我走了#{num}步')
step1(5)
再去运行,就报错了:
TypeError: wrapper() takes 0 positional arguments but 1 was given
这是因为,表面上我们写的是step1(5),实际上Python是先调用wrapper()函数。这个函数不接受参数,所以报错了。
为了解决这个问题,我们只要给wrapper加上参数就可以。
import time def timer(func): '''统计函数运行时间的装饰器''' def wrapper(*args, **kwargs): start = time.time()
func(*args, **kwargs)
end = time.time()
used = end - start
print(f'{func.__name__} used {used}') return wrapper
如果对关键词参数和位置参数不明白,可以在B站搜索"麦叔 参数",学习相关视频。
如果被装饰的函数func有返回值,wrapper也只需把func的返回值返回就可以了。
import time def timer(func): '''统计函数运行时间的装饰器''' def wrapper(*args, **kwargs): start = time.time()
ret_value = func(*args, **kwargs)
end = time.time()
used = end - start
print(f'{func.__name__} used {used}') return ret_value return wrapper @timer def add(num1, num2): return num1 + num2
sum = add(5, 8)
print(sum)
这里我新加了一个add函数,计算两个数之和。
在wrapper函数中,我们先保存了func的返回值到ret_value,然后在wrapper的最后返回这个值就可以了。
到这里,你又进了一步,你可以击败88.64%的Python学习者了。但还不够,后面还有:
有位同学看完前面的内容,觉得自己掌握的很好了,就去面试。
结果被面试官一个“如何在Python中实现单例模式”的问题给当场问倒了。
气得他上去就是两个耳刮子,不过不是打面试官,是打自己,恨自己没有等读透整篇再去面试。所以大家都耐心读完。
你一定用过装饰器Decorator
其实Decorator就在我们身边,只是我们可能不知道它们是装饰器。我来说几个:@classmethod @staticmethod @property
有没有一种"我靠"的冲动?!
对,这些很重要的语法,不过是装饰器的应用而已。
来看一个代码例子:
class Circle: #半径用下划线开头,表示私有变量 def __init__(self, radius): self._radius = radius #用property装饰器创建虚拟的半径属性 @property def radius(self): return self._radius #用setter装饰器给半径属性添加赋值操作 @radius.setter def radius(self, value): if value >= 0: self._radius = value else: raise ValueError("Radius must be positive") #用property装饰器创建虚拟的面积属性 @property def area(self): return self.pi() * self.radius**2 def cylinder_volume(self, height): return self.area * height #类方法 @classmethod def unit_circle(cls): return cls(1) #静态方法 @staticmethod def pi(): return 3.1415926535
你不要以为你已经掌握了装饰器,你只是听懂了。
从听懂到能动手写出来,再到被面试的时候,可以流畅的说出来,那还差着二十万八千里呢!
一定得多动手!所以抓紧时间,马上再来创建两个装饰器。
现在我们来创建一个装饰器:它会打印函数的参数,以及返回值。
如果你有实际项目经验,你一定会知道这个很有用。这不就是自动打印日志嘛!是程序员找臭虫的必备良药啊。
来看看代码:
def debug(func): def wrapper_debug(*args, **kwargs): print(f'{func.__name__}:{args}, {kwargs}')
ret_val = func(*args, **kwargs)
print(f'return: {ret_val}') return ret_val return wrapper_debug @debug def add(a, b): return a + b
add(1, 3)
add(2, 3)
add(4, 3)
在wrapper_debug函数中,我们先打印所有的参数,再调用原函数,最后先打印返回值,再返回返回值。这里并没有新的语法知识,就是为了练手。
曾经我还年轻,看到一个大神的代码里面有这么一行:
sleep(random(1,5))
因为有了这行代码,程序运行的时候挺慢的。我就问大神,为什么要这样。大神语重心长的跟我说:
你还年轻!我把这个程序交付给客户,客户会觉得有点慢,但还能忍。
忍不住了,会来找我优化性能。我一个手指头就把性能优化上去了,客户一定对我五体投地。而且我们公司的尾款也给我们了。
年轻人,多学着点!这就是阅历,阅历!
可惜我学了这么多年,也没学会这种阅历。
不过有时候,因为各种原因,我们确实需要让程序变慢一点。装饰器就排上了用场:
import time def slow(func): def wrapper_slow(*args, **kwargs): print(f'{func.__name__} sleeping 1 second')
time.sleep(1)
ret_val = func(*args, **kwargs) return ret_val return wrapper_slow @slow def add(a, b): return a + b
add(1, 3)
运行一下,你就会很有成就感!确实慢!
上面那个真实的段子,我劝大家和我一样,一直都学不会。日久见人心,坑人的事情不能干。
经过前面几个例子,我们可以总结出一个装饰器的模板。
按照这个模板,可以轻松写出装饰器:
def decorator(func): def wrapper_decorator(*args, **kwargs): #调用前操作 ret_val = func(*args, **kwargs) #调用后操作 return ret_val return wrapper_decorator
按照这个模板:
上面那两个都是普通的装饰器的应用,我们不能继续自High下去了。我们得学习新知识了。
上面那个slow的装饰器,如果能够传入到底要sleep几秒就好了,现在是固定的1秒,这个不香。
注意区分,这里的参数是指装饰器的参数。和前面提到的函数自身的参数是不同的。
我想让它多慢就多慢,然后我们再顷刻间扭转乾坤,这样客户就更为我神魂颠倒了。
要让装饰器接受参数,需要在普通装饰器的外面再套上一层:
import time def slow(seconds): def decorator_slow(func): def wrapper_slow(*args, **kwargs): print(f'{func.__name__} sleeping {seconds} second')
time.sleep(seconds)
ret_val = func(*args, **kwargs) return ret_val return wrapper_slow return decorator_slow #添加装饰器的时候可以传入要放慢几秒的参数。@slow(2)def add(a, b): return a + b #执行此行会停顿2秒 add(1, 3)
以前的装饰器,是函数里面有一个内部函数(2层函数),现在这个有了3层函数:
其实后面两层就是和之前一样的,唯一的区别是外面又加了一层。
为什么会这样呢?为什么最外面一层不需要传入func参数呢?
这是因为:
这就是说最外面一层的功能就是为了处理装饰器的参数的。
如果你一下子不能理解,先把代码敲出来,你就理解了。正所谓:熟读唐诗三百首,不会吟诗也会吟!
再来看一个装饰器带参数的例子:
def repeat(nums=3): def decorator_repeat(func): def wrapper_repeat(*args, **kwargs): for _ in range(nums):
func(*args, **kwargs) return wrapper_repeat return decorator_repeat @repeat(3) def run(): print('跑步有利于身体健康,来一圈') #这里会重复执行3次 run()
这个装饰和slow装饰器一样坑人,它会多次重复执行一个方法,并且可以动态指定要重复几次。
细细品味一下这个3层的函数,它是如何实现带参数的装饰器的。这两个例子都懂了,你就走在吊打面试官的路上了。
还记得前面给自己两个耳光的同学吗?如果他现在去面试,还是给自己两个耳光,还是不知道如何实现单例模式。
单例模式,是指一个类只能创建一个实例,是最常见的设计模式之一。
比如网站程序有一个类统计网站的访问人数,这个类只能有一个实例。如果每次访问都创建一个新的实例,那人数就永远是1了。
在Python中可以用装饰器实现单例模式。
前面的装饰器都是用来装饰函数的,或者用来装饰类方法的,比如我们写的slow, debug, timer; Python自带的staticmethod, classmethod等。
那如果把装饰器放到类名前面会怎样呢?来看这段代码:
from slow import slow @slow class Counter(): def __init__(self): self._count = 0 def visit(self): self._count += 1 print(f'visiting: {self._count}')
c1 = Counter()
c1.visit()
c1.visit()
c2 = Counter()
c2.visit()
c2.visit()
这个类名叫Counter(),顾名思义就是用来做计数的。它有一个内部变量叫做_count,每次调用Counter的visit()方法,计数就会加1.
第一行,我们引入了前面写的slow装饰器,是那个普通的不带参数的slow。装饰器就是个函数,当然可以被import进来。
这次@slow放在Counter类名前面,而不是方法的前面,会发生什么呢?运行上面的代码,会发现这样的结果:
Counter sleeping 1 second visiting: 1 visiting: 2 Counter sleeping 1 second visiting: 1 visiting: 2
这说明只有在创建Counter实例的时候,才会sleep一秒,调用visit函数的时候,不会sleep。
所以,类装饰器实际上装饰的是类的初始化方法。只有初始化的时候会装饰一次。
上面的运行结果很让人失望,如果去面试,还是会给自己两个耳刮子的。
作为一个计数器,应该计数是不断叠加的。
可是上面的代码,创建了两个计数器,自己记录自己的。扯淡啊!
我们现在就用类装饰器改造它:
def singleton(cls): '''创建一个单例模式''' def single_wrapper(*args, **kwargs): if not single_wrapper.instance:
single_wrapper.instance = cls(*args, **kwargs) return single_wrapper.instance
single_wrapper.instance = None return single_wrapper @singleton class Counter(): def __init__(self): self._count = 0 def visit(self): self._count += 1 print(f'visiting: {self._count}')
c1 = Counter()
c1.visit()
c1.visit()
c2 = Counter()
c2.visit()
c2.visit()
先来运行一下:
visiting: 1 visiting: 2 visiting: 3 visiting: 4
结果很满意,虽然创建了两个Counter,计数是记录在一起的。这主要得益于这个新的装饰器:
def singleton(cls): '''创建一个单例模式''' def single_wrapper(*args, **kwargs): #如果没有实例,则创建实例 if not single_wrapper.instance:
single_wrapper.instance = cls(*args, **kwargs) #返回原来的实例,或者新的实例 return single_wrapper.instance #给新创建的函数添加一个属性保存实例 single_wrapper.instance = None return single_wrapper
它和其他的装饰器基本一样,它的不同之处在于这一行:
single_wrapper.instance = None
在创建完函数后,又给函数添加了一个属性,用来保存实例,开始为None,就是没有实例。
再来分析一下代码逻辑:
把这个装饰器加到类上的时候,就相当于加到了初始化方法。
当我们创建Counter的时候,被这个装饰器截胡,它会返回一个已经创建好的实例。如果没有实例,它会创建一个。
也就是说,不管调用Counter()多少次,最终就只有一个实例。这就是实现了单例模式。
如果有点不懂,再看一遍,为的是在面试官面前扬眉吐气。
上面的例子中,我们看到装饰器自己保存了一个实例,你要的时候它就给你这一个,所以才实现了单例模式。这种就叫做带状态的装饰器。
我们再来看一个例子。count装饰器会记录一个函数被调用的次数:
def count(func): def wrapper_count(): wrapper_count.count += 1 print(f'{func.__name__}:第{wrapper_count.count}次调用')
func()
wrapper_count.count = 0 return wrapper_count @count def run(): print('跑步有利于身体健康,来一圈')
run()
run()
run()
运行结果:
run:第1次调用 跑步有利于身体健康,来一圈 run:第2次调用 跑步有利于身体健康,来一圈 run:第3次调用 跑步有利于身体健康,来一圈
关键点就在于这一行:
wrapper_count.count = 0
给wrapper_count函数添加了count属性,来记录函数调用的次数,它也是一个有状态的装饰器。
一个函数只能有一个装饰器吗?
装饰器的本质就是先调用装饰器,装饰器再调用函数。既然这样,那么多调用几层也无妨吧。
来看这个例子:
import time from slow import slow def timer(func): def wrapper(): start_time = time.perf_counter()
func()
end_time = time.perf_counter()
used_time = end_time - start_time
print(f'{func.__name__} used {used_time}') return wrapper @slow @timer def run(): print('跑步有利于身体健康,来一圈')
run()
这个例子中,run函数用了两个装饰器,slow和timer。它的执行过程就相当于:
数据分析咨询请扫描二维码
在准备数据分析师面试时,掌握高频考题及其解答是应对面试的关键。为了帮助大家轻松上岸,以下是10个高频考题及其详细解析,外加 ...
2024-12-20互联网数据分析师是一个热门且综合性的职业,他们通过数据挖掘和分析,为企业的业务决策和运营优化提供强有力的支持。尤其在如今 ...
2024-12-20在现代商业环境中,数据分析师是不可或缺的角色。他们的工作不仅仅是对数据进行深入分析,更是协助企业从复杂的数据信息中提炼出 ...
2024-12-20随着大数据时代的到来,数据驱动的决策方式开始受到越来越多企业的青睐。近年来,数据分析在人力资源管理中正在扮演着至关重要的 ...
2024-12-20在数据分析的世界里,表面上的技术操作只是“入门票”,而真正的高手则需要打破一些“看不见的墙”。这些“隐形天花板”限制了数 ...
2024-12-19在数据分析领域,尽管行业前景广阔、岗位需求旺盛,但实际的工作难度却远超很多人的想象。很多新手初入数据分析岗位时,常常被各 ...
2024-12-19入门数据分析,许多人都会感到“难”,但这“难”究竟难在哪儿?对于新手而言,往往不是技术不行,而是思维方式、业务理解和实践 ...
2024-12-19在如今的行业动荡背景下,数据分析师的职业前景虽然面临一些挑战,但也充满了许多新的机会。随着技术的不断发展和多领域需求的提 ...
2024-12-19在信息爆炸的时代,数据分析师如同探险家,在浩瀚的数据海洋中寻觅有价值的宝藏。这不仅需要技术上的过硬实力,还需要一种艺术家 ...
2024-12-19在当今信息化社会,大数据已成为各行各业不可或缺的宝贵资源。大数据专业应运而生,旨在培养具备扎实理论基础和实践能力,能够应 ...
2024-12-19阿里P8、P9失业都找不到工作?是我们孤陋寡闻还是世界真的已经“癫”成这样了? 案例一:本硕都是 985,所学的专业也是当红专业 ...
2024-12-19CDA持证人Louis CDA持证人基本情况 我大学是在一个二线城市的一所普通二本院校读的,专业是旅游管理,非计算机非统计学。毕业之 ...
2024-12-18最近,知乎上有个很火的话题:“一个人为何会陷入社会底层”? 有人说,这个世界上只有一个分水岭,就是“羊水”;还有人说,一 ...
2024-12-18在这个数据驱动的时代,数据分析师的技能需求快速增长。掌握适当的编程语言不仅能增强分析能力,还能帮助分析师从海量数据中提取 ...
2024-12-17在当今信息爆炸的时代,数据分析已经成为许多行业中不可或缺的一部分。想要在这个领域脱颖而出,除了热情和毅力外,你还需要掌握 ...
2024-12-17数据分析,是一项通过科学方法处理数据以获取洞察并支持决策的艺术。无论是在商业环境中提升业绩,还是在科研领域推动创新,数据 ...
2024-12-17在数据分析领域,图表是我们表达数据故事的重要工具。它们不仅让数据变得更加直观,也帮助我们更好地理解数据中的趋势和模式。相 ...
2024-12-16在当今社会,我们身处着一个飞速发展、变化迅猛的时代。不同行业在科技进步、市场需求和政策支持的推动下蓬勃发展,呈现出令人瞩 ...
2024-12-16在现代商业世界中,数据分析师扮演着至关重要的角色。他们通过解析海量数据,为企业战略决策提供有力支持。要有效完成这项任务, ...
2024-12-16在当今数据爆炸的时代,数据分析师是组织中不可或缺的导航者。他们通过从大量数据中提取可操作的洞察力,帮助企业在竞争激烈的市 ...
2024-12-16