百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

「Python终结」装饰器,也叫Decorator

off999 2024-12-03 00:12 27 浏览 0 评论

来源:麦叔编程

作者:麦叔

之前发过两篇装饰器的文章,当时为了博取眼球,标题取得虚头巴脑。现在把两篇合并在一起,重新发在『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函数就是个装饰器。

  1. 它的参数是需要被装饰的函数
  2. 返回值是新定义的一个包装了原有函数的函数。
  3. 新定义的函数先记录开始时间,调用被装饰的函数,然后再计算用了多少时间。

简单说就是把原来的函数给包了起来,在不改变原函数代码的情况下,在外面起到了装饰作用,这就是传说中的装饰器。它其实就是个普通的函数。

如果你觉得有点懵逼,需要加强一些对Python函数的理解。函数:

可以作为参数传递

可以作为返回值

也可以定义在函数内部

然后,我们不再直接调用step1, 而是:

  1. 先调用timer函数,生成一个包装了step1的新的函数timed_step1.
  2. 剩下的就是调用这个新的函数time_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
  1. wrapper使用了通配符,*args代表所有的位置参数,**kwargs代表所有的关键词参数。这样就可以应对任何参数情况。
  2. 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函数中,我们先打印所有的参数,再调用原函数,最后先打印返回值,再返回返回值。这里并没有新的语法知识,就是为了练手。

装B神奇 - 让程序跑慢点

曾经我还年轻,看到一个大神的代码里面有这么一行:

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

按照这个模板:

  1. 修改装饰器的名字,把decorator替换为具体的名字。
  2. 在注释“调用前操作”的地方写自己想写的代码
  3. 在注释“调用后操作”的地方写自己想写的代码。

带参数的装饰器

上面那两个都是普通的装饰器的应用,我们不能继续自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层函数:

  • 先是slow,接受秒数作为参数
  • slow里面创建了decorator_slow函数,这个就是和原来一样的装饰器函数
  • wrapper_slow里面又创建了wrapper_slow函数。

其实后面两层就是和之前一样的,唯一的区别是外面又加了一层。

为什么会这样呢?为什么最外面一层不需要传入func参数呢?

这是因为:

  1. 当Python发现slow(2)这个装饰器自带了参数时,它就不再传入当前函数作为参数,直接调用slow。这是Python解释器规定的。
  2. slow返回了一个函数,这时候Python会再把当前函数传入进去,这时候就成为一个普通的装饰器了。

这就是说最外面一层的功能就是为了处理装饰器的参数的。

如果你一下子不能理解,先把代码敲出来,你就理解了。正所谓:熟读唐诗三百首,不会吟诗也会吟!

再来看一个装饰器带参数的例子:

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,就是没有实例。

再来分析一下代码逻辑:

  1. 先判断是否有实例,如果没有就创建一个。反过来,已经有了就不用创建。
  2. 返回实例。

把这个装饰器加到类上的时候,就相当于加到了初始化方法。

当我们创建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。它的执行过程就相当于:

slow(time(run()))

从上到下调用,先是调用slow,然后slow去调用timer,然后timer去调用run,所以执行结果是:

run sleeping 1 second
跑步有利于身体健康,来一圈
wrapper_slow used 1.0026384350000002

Python装饰器宝藏库

差不多了,理解透这些原理,你就算不给面试官两个耳刮子,至少也不用给自己了。相关问题就算不是对答如流,也能轻松应对吧。

装饰器太重要了,有很多大神写了各种各样的装饰器,Python官方文档为了一份装饰器列表,在搜索引擎搜:PythonDecoratorLibrary

相关推荐

安全教育登录入口平台(安全教育登录入口平台官网)

122交通安全教育怎么登录:122交通网的注册方法是首先登录网址http://www.122.cn/,接着打开网页后,点击右上角的“个人登录”;其次进入邮箱注册,然后进入到注册页面,输入相关信息即可完...

大鱼吃小鱼经典版(大鱼吃小鱼经典版(经典版)官方版)

大鱼吃小鱼小鱼吃虾是于谦跟郭麒麟的《我的棒儿呢?》郭德纲说于思洋郭麒麟作诗的相声,最后郭麒麟做了一首,师傅躺在师母身上大鱼吃小鱼小鱼吃虾虾吃水水落石出师傅压师娘师娘压床床压地地动山摇。...

谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
  • 谷歌地球下载高清卫星地图(谷歌地球地图下载器)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)
哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)

要想将ppt免费转换为pdf的话,我们建议大家可以下一个那个wps,如果你是会员的话,可以注册为会员,这样的话,在wps里面的话,就可以免费将ppt呢转换为pdfpdf之后呢,我们就可以直接使用,不需要去直接不需要去另外保存,为什么格式转...

2026-02-04 09:03 off999

电信宽带测速官网入口(电信宽带测速官网入口app)

这个网站看看http://www.swok.cn/pcindex.jsp1.登录中国电信网上营业厅,宽带光纤,贴心服务,宽带测速2.下载第三方软件,如360等。进行在线测速进行宽带测速时,尽...

植物大战僵尸95版手机下载(植物大战僵尸95 版下载)

1可以在应用商店或者游戏平台上下载植物大战僵尸95版手机游戏。2下载教程:打开应用商店或者游戏平台,搜索“植物大战僵尸95版”,找到游戏后点击下载按钮,等待下载完成即可安装并开始游戏。3注意:确...

免费下载ppt成品的网站(ppt成品免费下载的网站有哪些)

1、Chuangkit(chuangkit.com)直达地址:chuangkit.com2、Woodo幻灯片(woodo.cn)直达链接:woodo.cn3、OfficePlus(officeplu...

2025世界杯赛程表(2025世界杯在哪个国家)

2022年卡塔尔世界杯赛程公布,全部比赛在卡塔尔境内8座球场举行,2022年,决赛阶段球队全部确定。揭幕战于当地时间11月20日19时进行,由东道主卡塔尔对阵厄瓜多尔,决赛于当地时间12月18日...

下载搜狐视频电视剧(搜狐电视剧下载安装)

搜狐视频APP下载好的视频想要导出到手机相册里方法如下1、打开手机搜狐视频软件,进入搜狐视频后我们点击右上角的“查找”,找到自已喜欢的视频。2、在“浏览器页面搜索”窗口中,输入要下载的视频的名称,然后...

pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
  • pubg免费下载入口(pubg下载入口官方正版)
永久免费听歌网站(丫丫音乐网)

可以到《我爱音乐网》《好听音乐网》《一听音乐网》《YYMP3音乐网》还可以到《九天音乐网》永久免费听歌软件有酷狗音乐和天猫精灵,以前要跳舞经常要下载舞曲,我从QQ上找不到舞曲下载就从酷狗音乐上找,大多...

音乐格式转换mp3软件(音乐格式转换器免费版)

有两种方法:方法一在手机上操作:1、进入手机中的文件管理。2、在其中选择“音乐”,将显示出手机中的全部音乐。3、点击“全选”,选中所有音乐文件。4、点击屏幕右下方的省略号图标,在弹出菜单中选择“...

电子书txt下载(免费的最全的小说阅读器)

1.Z-library里面收录了近千万本电子书籍,需求量大。2.苦瓜书盘没有广告,不需要账号注册,使用起来非常简单,直接搜索预览下载即可。3.鸠摩搜书整体风格简洁清晰,书籍资源丰富。4.亚马逊图书书籍...

最好免费观看高清电影(播放免费的最好看的电影)

在目前的网上选择中,IMDb(互联网电影数据库)被认为是最全的电影网站之一。这个网站提供了各种类型的电影和电视节目的海量信息,包括剧情介绍、演员表、评价、评论等。其还提供了有关电影制作背后的详细信息,...

孤单枪手2简体中文版(孤单枪手2简体中文版官方下载)

要将《孤胆枪手2》游戏的征兵秘籍切换为中文,您可以按照以下步骤进行操作:首先,打开游戏设置选项,通常可以在游戏主菜单或游戏内部找到。然后,寻找语言选项或界面选项,点击进入。在语言选项中,选择中文作为游...

取消回复欢迎 发表评论: