关于python:Python-终结者-装饰器也叫-Decorator

6次阅读

共计 9512 个字符,预计需要花费 24 分钟才能阅读完成。

对于 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”即可收费取得

正文完
 0