头图

Python 终结者 - 装饰器,也叫 Decorator

对于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, 而是:

  • 先调用timer函数,生成一个包装了step1的新的函数timed_step1.
  • 剩下的就是调用这个新的函数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
  • wrapper使用了通配符,args代表所有的位置参数,*kwargs代表所有的关键词参数。这样就可以应对任何参数情况。
  • wrapper调用被装饰的函数的时候,只要原封不动的把参数再传递进去就可以了。

函数返回值

如果被装饰的函数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

按照这个模板:

  • 修改装饰器的名字,把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层函数:

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

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

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

这是因为:

  • 当Python发现slow(2)这个装饰器自带了参数时,它就不再传入当前函数作为参数,直接调用slow。这是Python解释器规定的。
  • 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,就是没有实例。

再来分析一下代码逻辑:

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

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

当我们创建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装饰器宝藏库
差不多了,理解透这些原理,你就算不给面试官两个耳刮子,至少也不用给自己了。相关问题就算不是对答如流,也能轻松应对吧。最近整理了几百 G 的 Python 学习资料,包含新手入门电子书、教程、源码等等,免费分享给大家!想要的前往 “Python 编程学习圈”,发送 “J” 即可免费获得

全球Python编程中文开发者的圈子,提供Python编程技术文章知识分享、技术专栏、原创视频教程、题库,内...

10 声望
1 粉丝
0 条评论
推荐阅读
终结 Python 原生字典?这个库要逆天改命了
字典是 Python 中基础的数据结构之一,字典的使用,可以说是非常的简单粗暴,但即便是这样一个与世无争的数据结构,仍然有很多人 "看不惯它" 。

Python编程学习圈阅读 303

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.3k

滚蛋吧,正则表达式!
你是不是也有这样的操作,比如你需要使用「电子邮箱正则表达式」,首先想到的就是直接百度上搜索一个,然后采用 CV 大法神奇地接入到你的代码中?

良许3阅读 1.5k

搭个ChatGPT算法模型,从哪开始?
最近 ChatGPT 很火,火到了各行各业。记得去年更多的还是码农最新体验后拿它搜代码,现在各行各业都进来体验,问它咋理财、怎么写报告和给小孩起名。😂 也因此让小傅哥在头条的一篇关于 ChatGPT 的文章都有了26万...

小傅哥6阅读 1.2k

封面图
程序员适合创业吗?
大家好,我是良许。从去年 12 月开始,我已经在视频号、抖音等主流视频平台上连续更新视频到现在,并得到了不错的评价。每个视频都花了很多时间精力用心制作,欢迎大家关注哦~考虑到有些小伙伴没有看过我的视频,...

良许3阅读 1.3k

Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 4.6k评论 1

PyCharm 激活破解教程, 2023 年 2 月亲测有用
本文分享一下PyCharm 2022.2.3 版本最新激活破解教程,注意不要使用太新的版本,都是 Jetbrains 产品,本文专门配上了 Pycharm 的图片,跟着下面教程一步一步来即可。

程序员徐公阅读 9.4k评论 1

全球Python编程中文开发者的圈子,提供Python编程技术文章知识分享、技术专栏、原创视频教程、题库,内...

10 声望
1 粉丝
宣传栏