Python 中提供了一个叫装饰器的特性,用于在不改变原始对象的情况下,增加新功能或行为。

这也属于 Python "元编程" 的一部分,在编译时一个对象去试图修改另一个对象的信息,实现 "控制一切" 目的。

本篇文章作为装饰器的基础篇,在阅读后应该了解如下内容:

  • 装饰器的原理?
  • 装饰器如何包裹有参数的函数?
  • 装饰器本身需要参数怎么办?
  • 被装饰器修饰的函数还是原函数吗,怎么解决?
  • 装饰器嵌套时的顺序?
  • 装饰器常见的应用场景?

装饰器原理

在具体装饰器的内容前,先来回顾下 Python 中的基本概念:

Python 中,一切都是对象,函数自然也不例外

python 中的对象都会在内存中用于属于自己的一块区域。在操作具体的对象时,需要通过 “变量” ,变量本身仅是一个指针,指向对象的内存地址。

函数作为对象的一种,自然也可以被变量引用。

def hello(name: str):
print('hello', name)
hello('Ethan') alias_func_name = hello
alias_func_name('Michael')
# hello Ethan
# hello Michael

alias_func_name 作为函数的引用,当然也可以作为函数被使用。

函数接受的参数和返回值都可以是函数

def inc(x):
return x + 1 def dec(x):
return x - 1 def operate(func, x):
result = func(x)
return result operate(inc,3)
# 4
operate(dec,3)
# 2

这里 operate 中接受函数作为参数,并在其内部进行调用。

嵌套函数

def increment():
def inner_increment(number):
return 1 + number
return inner_increment() print(increment(100)) # 101

在 increment 内部,实现对 number add 1 的操作。

回头再来看下装饰器的实现:

# def decorator
def decorator_func(func):
print('enter decorator..')
def wrapper():
print('Step1: enter wrapper func.')
return func()
return wrapper # def target func
def normal_func():
print("Step2: I'm a normal function.") # use decorator
normal_func = decorator_func(normal_func)
normal_func()

decorator_func(func) 中,参数 func 表示想要调用的函数,wrapper 为嵌套函数,作为装饰器的返回值。

wrapper 内部会调用目标函数 func 并附加自己的行为,最后将 func 执行结果作为返回值。

究其根本,是在目标函数外部套上了一层 wrapper 函数,达到在不改变原始函数本身的情况下,增加一些功能或者行为。

通常使用时,使用 @decorator_func 来简化调用过程的两行代码。

将自定义调用装饰器的两行代码删掉,使用常规装饰器的写法加在 normal_func 的定义处,但却不调用 normal_func,可以发现一个有趣的现象:

# def decorator
def decorator_func(func):
print('enter decorator..')
def wrapper():
print('Step1: enter wrapper func.')
return func()
return wrapper # def target func
@decorator_func
def normal_func():
print("Step2: I'm a normal function.")

发现 enter decorator.. 在没有调用的情况下被打印到控制台。

这就说明,此时 normal_func 已经变成了 wrapper 函数。

@decorator_func 其实隐含了 normal_func = decorator_func(normal_func) 这一行代码。

对带有参数的函数使用装饰器

假设这里 normal_func 需要接受参数怎么办?

很简单,由于是通过嵌套函数来调用目标函数,直接在 wrapper 中增加参数就可以了。

# def decorator
def decorator_func(func):
def wrapper(*args, **kwargs):
print('Step1: enter wrapper func.')
return func(*args, **kwargs)
return wrapper # def target func
def normal_func(*args, **kwargs):
print("Step2: I'm a normal function.")
print(args)
print(kwargs) # use decorator
normal_func = decorator_func(normal_func)
normal_func(1, 2, 3, name='zhang', sex='boy')

使用 *args, **kwargs 是考虑到该 decorator 可以被多个不同的函数使用,而每个函数的参数可能不同。

装饰器本身需要参数

在装饰器本身也需要参数时,可以将其嵌套在另一个函数中,实现参数的传递。

# def decorator
def decorator_with_args(*args, **kwargs):
print('Step1: enter wrapper with args func.')
print(args)
print(kwargs) def decorator_func(func):
def wrapper(*args, **kwargs):
print('Step2: enter wrapper func.')
return func(*args, **kwargs)
return wrapper
return decorator_func # def target func
def normal_func(*args, **kwargs):
print("Step3: I'm a normal function.")
print(args)
print(kwargs) normal_func = decorator_with_args('first args')(normal_func)
normal_func('hello') # use @ to replace the above three lines of code
@decorator_with_args('first args')
def normal_func(*args, **kwargs):
print("Step3: I'm a normal function.")
print(args)
print(kwargs)

来分析下 decorator_with_args 函数:

  • 由于 decorator_with_args 接受了任意数量的参数,同时由于 decorator_funcwrapper 作为其内部嵌套函数,自然可以访问其内部的作用域的变量。这样就实现了装饰器参数的自定义。
  • decorator_func 是正常的装饰器,对目标函数的行为进行包装。进而需要传递目标函数作为参数。

在使用时:

@decorator_with_args('first args') 实际上做的内容,就是 normal_func = decorator_with_args('first args')(normal_func) 的内容:

  1. decorator_with_args('first args') 返回 decorator_func 装饰器。
  2. decorator_func 接受的正常函数对象作为参数,返回包装的 wrapper 对象。
  3. 最后将 wrapper 函数重命名至原来的函数,使其在调用时保持一致。

保留原函数信息

在使用装饰器时,看起来原函数并没有被改变,但它的元信息却改变了 - 此时的原函数实际是包裹后的 wrapper 函数。

help(normal_func)
print(normal_func.__name__) # wrapper(*args, **kwargs)
# wrapper

如果想要保留原函数的元信息,可通过内置的 @functools.wraps(func) 实现:

@functools.wraps(func) 的作用是通过 update_wrapperpartial 将目标函数的元信息拷贝至 wrapper 函数。

# def decorator
def decorator_with_args(*args, **kwargs):
print('Step1: enter wrapper with args func.')
print(args)
print(kwargs) def decorator_func(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('Step2: enter wrapper func.')
return func(*args, **kwargs)
return wrapper
return decorator_func

装饰器嵌套

Python 支持对一个函数同时增加多个装饰器,那么添加的顺序是怎样的呢?

# def decorator
def decorator_func_1(func):
print('Step1: enter decorator_func_1..') def wrapper():
print('Step2: enter wrapper1 func.')
return func()
return wrapper def decorator_func_2(func):
print('Step1: enter decorator_func_2..') def wrapper():
print('Step2: enter wrapper2 func.')
return func()
return wrapper @decorator_func_2
@decorator_func_1
def noraml_func():
pass

看一下 console 的结果:

Step1: enter decorator_func_1..
Step1: enter decorator_func_2..

fun_1 在前说明, 在对原函数包装时,采用就近原则,从下到上。

接着,调用 noraml_func 函数:

Step1: enter decorator_func_1..
Step1: enter decorator_func_2..
Step2: enter wrapper2 func.
Step2: enter wrapper1 func.

可以发现,wrapper2 内容在前,说明在调用过程中由上到下。

上面嵌套的写法,等价于 normal_func = decorator_func_2(decorator_func_1(normal_func)),就是正常函数的调用过程。

对应执行顺序:

  1. 在定义时,先 decorator_func_1 后 decorator_func_2.
  2. 在调用时,先 decorator_func_2 后 decorator_func_1.

应用场景

日志记录

在一些情况下,需要对函数执行的效率进行统计或者记录一些内容,但又不想改变函数本身的内容,这时装饰器是一个很好的手段。

import timeit
def timer(func):
def wrapper(n):
start = timeit.default_timer()
result = func(n)
stop = timeit.default_timer()
print('Time: ', stop - start)
return result
return wrappe

作为缓存

装饰器另外很好的应用场景是充当缓存,如 lru 会将函数入参和返回值作为当缓存,以计算斐波那契数列为例, 当 n 值大小为 30,执行效率已经有很大差别。

def fib(n):
if n < 2:
return 1
else:
return fib(n - 1) + fib(n - 2) @functools.lru_cache(128)
def fib_cache(n):
if n < 2:
return 1
else:
return fib_cache(n - 1) + fib_cache(n - 2) Time: 0.2855725
Time: 3.899999999995574e-05

总结

在这一篇中,我们知道:

装饰器的本质,就是利用 Python 中的嵌套函数的特点,将目标函数包裹在内嵌函数中,然后将嵌套函数 wrapper作为返回值返回,从而达到修饰原函数的目的。

而且由于返回的是 wrapper 函数,自然函数的元信息肯定不再是原函数的内容。

对于一个函数被多个装饰器修饰的情况:

  • 在包装时,采用就近原则,从近点开始包装。
  • 在被调用时,采用就远原则,从远点开始执行。

这自然也符合栈的调用过程。

参考

https://www.programiz.com/python-programming/decorator

Python 元编程 - 装饰器的更多相关文章

  1. Python核心编程 | 装饰器

        装饰器是程序开发的基础知识,用好装饰器,在程序开发中能够提高效率 它可以在不需要修改每个函数内部代码的情况下,为多个函数添加附加功能,如权限验证,log日志等   涉及点:   1.先梳理一下 ...

  2. python函数式编程-装饰器

    在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator). 由于函数也是一个对象,而且函数对象可以赋值给变量,所以通过变量也能调用该函数. >>> def now() ...

  3. 面向切面编程AOP——加锁、cache、logging、trace、同步等这些较通用的操作,如果都写一个类,则每个用到这些功能的类使用多继承非常难看,AOP就是解决这个问题的,python AOP就是装饰器

    面向切面编程(AOP)是一种编程思想,与OOP并不矛盾,只是它们的关注点相同.面向对象的目的在于抽象和管理,而面向切面的目的在于解耦和复用. 举两个大家都接触过的AOP的例子: 1)java中myba ...

  4. Day04 - Python 迭代器、装饰器、软件开发规范

    1. 列表生成式 实现对列表中每个数值都加一 第一种,使用for循环,取列表中的值,值加一后,添加到一空列表中,并将新列表赋值给原列表 >>> a = [0, 1, 2, 3, 4, ...

  5. Python元编程

    简单定义"元编程是一种编写计算机程序的技术,这些程序可以将自己看做数据,因此你可以在运行时对它进行内审.生成和/或修改",本博参考<<Python高级编程>> ...

  6. python元编程(metaclass)

    Python元编程就是使用metaclass技术进行编程,99%的情况下不会使用,了解即可. Python中的类和对象 对于学习Python和使用Python的同学,你是否好奇过Python中的对象究 ...

  7. python高级之装饰器

    python高级之装饰器 本节内容 高阶函数 嵌套函数及闭包 装饰器 装饰器带参数 装饰器的嵌套 functools.wraps模块 递归函数被装饰 1.高阶函数 高阶函数的定义: 满足下面两个条件之 ...

  8. [python基础]关于装饰器

    在面试的时候,被问到装饰器,在用的最多的时候就@classmethod ,@staticmethod,开口胡乱回答想这和C#的static public 关键字是不是一样的,等面试回来一看,哇,原来是 ...

  9. Python深入05 装饰器

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 装饰器(decorator)是一种高级Python语法.装饰器可以对一个函数.方法 ...

随机推荐

  1. ALGO基础(一)—— 排序

    ALGO基础(一)-- 排序 冒选插希快归堆,以下均为从小到大排 1 冒泡排序 描述: 比较相邻的元素.如果第一个比第二个大,就交换它们两个: 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一 ...

  2. python爬虫登录保持及对http总结

    [前言]这几天一直看python爬虫登录保持.实现接口太多,太乱,新手难免云山雾罩.各种get.post,深入理解一下,其实就是由于http的特性需要这些操作.http是一种无状态.不保存上次通信结果 ...

  3. Python 过滤字母和数字

    [前言]在写爬虫时,正则表达式有时候比较难写,一个是自己不熟练,二者数据分析提取数据千奇百怪. 一.好在python有个re模块,提供了很多更加简便的方法:可参考此文档:https://www.cnb ...

  4. wxWidgets源码分析(7) - 窗口尺寸

    目录 窗口尺寸 概述 窗口Size消息的处理 用户调整Size消息的处理 调整窗口大小 程序调整窗口大小 wxScrolledWindow设置窗口大小 获取TextCtrl控件最合适大小 窗口尺寸 概 ...

  5. if...else和switch...case

    一.位运算 class Demo01 { public static void main(String[] args) { int a = 5; int b = 3; /* 0000 0101 |00 ...

  6. 心脏滴血(CVE-2014-0160)检测与防御

    用Nmap检测 nmap -sV --script=ssl-heartbleed [your ip] -p 443 有心脏滴血漏洞的报告: ➜ ~ nmap -sV --script=ssl-hear ...

  7. 记录core中GRPC长连接导致负载均衡不均衡问题 二,解决长连接问题

    题外话: 1.这几天收到蔚来的面试邀请,但是自己没做准备,并且远程面试,还在上班时间,再加上老东家对我还不错.没想着换工作,导致在自己工位上做算法题不想被人看见,然后非常紧张.估计over了.不过没事 ...

  8. 1.2 Python3基础-规范

    >>返回主目录 总的来说,如果安装的不是安装的Anaconda,pip命令还是经常会用到的(cmd模式使用),当然也可以在PyCharm中直接安装 PEP8规范,我另有一篇博客已经写好,可 ...

  9. ZooKeeper 基本概念并介绍RPC中Netty和Zookeeper的使用

    前言 ZooKeeper 是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等.Zookeeper提供一个类似Linux文件系统的属性结构,每个节点可存储少量的内存文件,并提供每 ...

  10. 使用wireshark 抓取 http https tcp ip 协议进行学习

    使用wireshark 抓取 http https tcp ip 协议进行学习 前言 本节使用wireshark工具抓包学习tcp ip http 协议 1. tcp 1.1 tcp三次握手在wire ...