社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

二十一、深入Python强大的装饰器

Python开发 • 5 年前 • 530 次点击  
👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

作者丨小sen

来源丨Python之王


「@Author:Runsen」

最近有同学在问关于Python中装饰器的问题,说不太理解装饰器的装饰过程。

那么在下面Runsen来给大家深入讲解一下装饰器的整个实现过程的。

闭包

想要理解Python中的装饰器,不得不先理解闭包(closure)这一概念。

闭包就应该想起了嵌套函数,也可以将闭包理解为一种特殊的函数,这种函数由两个函数的嵌套组成,外函数和内函数。

def 外层函数(参数):
    def 内层函数():
        print("内层函数执行", 参数)

    return 内层函数

内层函数的引用 = 外层函数("传入参数")
内层函数的引用()

在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包。

下面举一个具体的闭包函数的实例,代码如下。

# outer是外部函数
def outer(a):
    # inner是内函数
    def inner( b ):
        #在内函数中 用到了外函数的临时变量
        print(a+b)
    # 外函数的返回值是内函数的引用
    return inner
ret = outer(5) #ret = inner
ret(10) #15 ret 存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数

装饰器

装饰器,顾名思义,就是用来“装饰”的。比如@Runsen就是一个装饰器,其中"Runsen"是你的装饰器的名字。它能装饰的东西有:函数、类。装饰器一般在函数、类的上面用@符号定义。

装饰器本质上是一个Python函数(一定有参数),如果严格来说,装饰器只是语法糖,也可以将装饰器叫做一种特殊的闭包。

装饰器是可调用的对象,可以像常规的可调用对象那样调用,特殊的地方是装饰器的参数是一个函数名。

下面就是最简单的装饰器,代码来自Python3官方文档。

def warp(obj):
    return obj
        
@warp    # 等价于 foo = warp(foo)
def foo():    
    print('hello decorator!')
 
foo()    # => hello decorator!  

上面使用了装饰器的代码,其实我们可以通过其它方式达到相同的效果,具体见下。

def foo():
    print('hello decorator!')
 
foo = warp(foo)
foo()    # => hello decorator!

嵌套函数的装饰器

在上面代码中装饰器都只是一个普通的函数,如果该函数是嵌套函数,那么函数传入的参数,在内部函数依然可以使用。

先看两段代码,在这里my_decorator就是一个装饰器。

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

def greet():
    print('hello world')

greet = my_decorator(greet)
greet()

# 输出
wrapper of decorator
hello world

my_decorator函数传入greet函数名方法,中间有一个wrapper内函数方法,  而return wrapper说明要执行wrapper内函数,wrapper内函数,于是执行greet函数名方法。

其实,greet = my_decorator(greet)这个代码可以用装饰器来替代,在greet上面加一个@my_decorator,然后直接执行greet(),最终输出一样。

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper

@my_decorator
def greet():
    print('hello world')

greet()

wrapper of decorator
hello world

装饰器就是继承了 my_decorator函数,因此先调用my_decorator 中的wrapper打印出wrapper of decorator,然后func()被调用,传入的参数是greet,因此指的就是greet(),所以在打印出hello world

带参数嵌套函数的装饰器

有时候嵌套函数需要传入参数到内部函数,这时候用*args, **kwargs接受就可以了。*args接收元组,**kwargs接受字典。

下面,我们来看一个例子。

# repeat重复输出,num指重复输出次数
def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                print('wrapper of decorator')
                func(*args, **kwargs)
        return wrapper
    return my_decorator


@repeat(4)
def greet(message):
    print(message)

greet('hello world')

# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world

上面代码的意思:@repeat(4)将会执行greet(4),由于存在return my_decorator,所以下一步执行my_decorator(greet).由于又存在return wrapper。所以下一步将会执行wrapper(*args, **kwargs)。这里的*args, **kwargs指的是hello world字符串。因此最终打印四次wrapper of decoratorhello world

但是自定义参数的装饰器将改变函数本身的元信息,即函数不再是本身的函数

greet.__name__
## 输出
'wrapper'

这时,需要使用内置模块functools.wrap会保留原函数的元信息。

import functools

def repeat(num):
    def my_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(num):
                print('wrapper of decorator')
                func(*args, **kwargs)
        return wrapper
    return my_decorator

@repeat(4)
def greet(message):
    print(message)

greet.__name__

# 输出 不是wrapper
'greet' 

类装饰器

类装饰器主要依赖函数__call__ ,因此我们主要重写__call__即可。

每当调用一个类的实例,函数__call__就会执行一次。

下面,我们来看一个例子。

class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)

@Count
def example():
    print("hello world")

example()

# 输出
num of calls is1
hello world

example()

# 输出
num of calls is2
hello world

嵌套装饰器

我们可以把多个装饰器叠加在同一个函数上,这个就叫做嵌套装饰。

嵌套装饰器的顺序是从下到上,

@f2
@f1
def greet(name):
    print(f"Hello {name}")

所以上面的例子就是等价于:greet = f2(f1(greet))

那么相当于,从里到外。

import functools

def my_decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator1')
        func(*args, **kwargs)
    return wrapper


def my_decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator2')
        func(*args, **kwargs)
    return wrapper


@my_decorator1
@my_decorator2
def greet(message):
    print(message)

greet('hello world')  #相当于my_decorator1(my_decorator2(greet('hello world')))

# 输出
execute decorator1
execute decorator2
hello world

Reference

[1]

传送门~: https://github.com/MaoliRUNsen/runsenlearnpy100


-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里 好文分享给更多人↓↓

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/113852