本文首发于公众号:Hunter后端

原文链接:Python笔记三之闭包与装饰器

这一篇笔记介绍 Python 里面的装饰器。

在介绍装饰器前,首先提出这样一个需求,我想统计某个函数的执行时间,假设这个函数如下:

import time

def add(x, y):
time.sleep(1)
return x + y

想要统计 add 函数的执行时间,可以如何操作,在一般情况下,可能会想到如下操作:

start_time = time.time()
add(1, 2)
end_time = time.time()
print("函数执行时间为:", end_time - start_time)

而如果我们想要统计很多个函数的执行时间,然后打印出来,应该如何操作呢?

这里就可以用上 Python 里装饰器的操作。

本篇笔记目录如下:

  1. 闭包

    1. 闭包
    2. 闭包实现计数器
      1. 自由变量
  2. 装饰器
  3. 装饰器代码示例装饰器原理
  4. 装饰器加参数
  5. 多重装饰器
  6. 装饰器类

1、闭包

在介绍装饰器前,先来理解一下闭包的概念。

1. 闭包

我们知道,一个函数内部的变量是局部变量,在函数执行结束之后,函数内部的变量就会被销毁,而闭包,则可以使我们能够读取函数内部变量。

比如下面这个示例:

def outer_func():
msg = "outer info" def inner_func():
print(msg)
return msg return inner_func func = outer_func()
func()

关于闭包,2023.11.13 百度百科的释义如下:

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

所以闭包的作用可以是避免全局变量可能带来的维护问题,又能够长久的保存变量。

但是同时,基于这个特性,闭包函数内部的局部变量因为会保持在内存中,不会在调用后被自动清除,所以需要注意其可能带来的内存泄漏的问题。

2. 闭包实现计数器

下面我们使用闭包来实现一个计数器的功能:

def create_counter():
count = 0 def add_counter():
nonlocal count
count += 1
return count
return add_counter f = create_counter()
print(f())
print(f())
print(f())

这里使用 nolocal 对 count 变量进行了声明,作用是声明该变量只在函数局部内起作用,也就是 create_counter() 内,所以在 add_counter() 外声明 count 变量之后,在 add_counter() 内可以保存其相应的状态,也就是这里我们的计数功能。

nolocal 关键字是专门定义在闭包内使用的。

相对应的 global 字段时定义的全局变量,这里不多做介绍了。

自由变量

自由变量的含义是指未绑定到本地作用域的变量,比如上面的示例里,count 在 add_counter() 函数里就是一个自由变量,因为它在外层函数 create_counter() 里定义,但没有在内层的 add_counter() 中定义。

至于为什么在 add_counter() 里对 count 变量进行 nolocal 的声明,是因为修饰的对象类型是 int,与之类似的还有 str、tuple,他们都属于不可变类型。

而如果我们闭包的内外部函数里的对象是 list,dict 这种可变类型,那么则不需要使用 nolocal 来进行修饰,比如下面的操作:

def create_counter():
count_dict = [0]   def add_counter():       
count_dict[0] += 1       
return count_dict[0]    return add_counter

2、 装饰器

装饰器的作用是在不修改被装饰函数的情况下,给被装饰的函数添加额外的功能。

而装饰器就是基于闭包的操作,不过外层函数传入的参数是被装饰的函数,且在 Python 里,使用装饰器的方式是在被装饰函数前加一行,使用 @ 符号来调用。

最简单的装饰器的操作如下:

def decorator(func):
print("calling decorator ...")
return func @decorator
def test():
print("calling test ...")

我们在下面的操作中使用一个示例介绍如何基于闭包使用装饰器。

3、装饰器代码示例

前面我们介绍了一个需求场景,需要统计函数的执行时间,基于这个需求,我们就可以使用装饰器的操作来完成,以下是代码示例:

import time

def time_decorator(func):

    def inner_func(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
total_time = time.time() - start_time
print("func 耗时:", total_time)
return result
return inner_func @time_decorator
def add(x, y):
time.sleep(1)
return x + y add(1, 7)

装饰器原理

我们使用 @ 加上装饰器函数名称,即表示调用这个装饰器,然后将被装饰的函数,上面的示例是 add() 函数,作为参数传入装饰器,然后在内部函数 inner_func() 中添加额外的功能,这里是统计函数运行时间,然后将其返回。

将装饰器的操作扁平化操作,就和前面闭包示例计数器的使用是一致的:

def add(x, y):
time.sleep(1)
return x + y func = time_decorator(add)
func(1, 2)

所以,在加了装饰器的函数运行中,实际上运行的是装饰器的内部函数,我们可以通过打印函数的名称来进行验证:

print(add.__name__)  # inner_func

如果想要保存原函数的基本信息,比如函数名称,我们可以给装饰器的内部函数加上装饰器自动复制函数信息,functools.wraps,使用示例如下:

import time
import functools def time_decorator(func): @functools.wraps(func)
def inner_func(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
total_time = time.time() - start_time
print("func 耗时:", total_time)
return result
return inner_func @time_decorator
def add(x, y):
time.sleep(1)
return x + y print(add.__name__) # add

这样打印的就是原始函数的函数名称了。

4、装饰器加参数

如果我们想调用装饰器的时候,给装饰器加一个参数,比如这里的 time_decorator,想加一个默认的时间参数(这个想要实现的功能可能并没有实际意义,纯粹是为了实现给装饰器加默认参数这个功能),调用的时候就是:

@time_decorator(default_time=2)

那么装饰器的定义则如下所示:

def time_decorator(default_time=2):
def decorator(func):
def inner_func(*args, **kwargs):
start_time = time.time()
time.sleep(default_time)
result = func(*args, **kwargs)
total_time = time.time() - start_time
print("func 耗时:", total_time)
return result
return inner_func
return decorator @time_decorator(2)
def add(x, y):
time.sleep(1)
return x + y add(1, 8)

如果调用装饰器的时候想使用默认参数,直接不赋值即可:

@time_decorator()
def add(x, y):
time.sleep(1)
return x + y

5、多重装饰器

如果我们想要调用多个装饰器来装饰一个函数,其执行顺序是怎么要的呢,我们可以用下面的例子做个实验。

比如我们要做一个汉堡,最外层两片面包,中间夹两片青菜,最中间是一片肉,可以如下操作:

def bread_decorator(func):

    def inner(*args, **kwargs):
print("先加片面包")
func(*args, **kwargs)
print("再加片面包")
return inner def vegetable_decorator(func): def inner(*args, **kwargs):
print("先加片蔬菜")
func(*args, **kwargs)
print("再加片蔬菜")
return inner @bread_decorator
@vegetable_decorator
def make_hamburger():
print("加片肉") make_hamburger()

输出的结果为:

先加片面包
先加片蔬菜
加片肉
再加片蔬菜
再加片面包

所以这里装饰器的执行时按照顺序从上到下执行的。

我们可以尝试将装饰器的调用拉平,用到的其实就是设计模式里的装饰器模式了(设计模式的几种类型我回头会更新一个系列),我们先将 make_hamburger() 的函数重新定义,然后调用,bread_decorator() 和 vege_decorator() 还是保持不变:

def make_hamburger():   
print("加片肉") food = vegetable_decorator(make_hamburger)
food = bread_decorator(food)
food()

执行的结果和前面使用装饰器的方式调用是一致的。

6、装饰器类

前面介绍的是用函数作为装饰器,我们还可以设计一个类用作装饰器,示例如下:

class TimeLogDecorator:
def __init__(self, func):
self.func = func def __call__(self, *args, **kwargs): start_time = time.time()
result = self.func(*args, **kwargs)
print(f"函数 {self.func.__name__} 运行时间为:{time.time() - start_time}")
return result @TimeLogDecorator
def add(x, y):
time.sleep(1)
return x + y result = add(1, 6)

在类的 __call__ 方法写入我们在函数装饰器的内部函数里的内容即可实现装饰器的功能。

如果想要给类装饰器带参数的话,示例如下:

class TimeLogDecoratorArg:
def __init__(self, base_gap_time):
self.base_gap_time = base_gap_time def __call__(self, func): def inner_func(*args, **kwargs):
start_time = time.time()
time.sleep(self.base_gap_time)
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 运行时间为:{time.time() - start_time}")
return result
return inner_func @TimeLogDecoratorArg(2)
def add(x, y):
time.sleep(1)
return x + y

如果想获取更多相关文章,可扫码关注阅读:

Python笔记三之闭包与装饰器的更多相关文章

  1. python基础(三)闭包与装饰器

    闭包(closure): 内嵌函数通过调用外部嵌套函数作用域内的变量,则这个内嵌函数就是闭包. 闭包必须满足三个条件: 必须有一个内嵌函数 内嵌函数必须引用外部嵌套函数中的变量 外部函数的返回值必须是 ...

  2. Python之命名空间、闭包、装饰器

    一.命名空间 1. 命名空间 命名空间是一个字典,key是变量名(包括函数.模块.变量等),value是变量的值. 2. 命名空间的种类和查找顺序 - 局部命名空间:当前函数 - 全局命名空间:当前模 ...

  3. Python 变量作用域,闭包和装饰器

    from dis import dis b = 6 def f1(a): print(a)print(b) b = 9 f1(3) print(dis(f1)) # dis模块可以查看python函数 ...

  4. python函数作用域,闭包,装饰器

    第一:函数作用域: L:local 函数内部作用域 E:enclosing       函数内部与内嵌函数之间(闭包) G:global            全局作用域 B:build_in    ...

  5. Python之面向对象:闭包和装饰器

    一.闭包 1. 如果一个函数定义在另一个函数的作用域内,并且引用了外层函数的变量,则该函数称为闭包. def outter(): name='python' def inner(): print na ...

  6. Python函数进阶:闭包、装饰器、生成器、协程

    返回目录 本篇索引 (1)闭包 (2)装饰器 (3)生成器 (4)协程 (1)闭包 闭包(closure)是很多现代编程语言都有的特点,像C++.Java.JavaScript等都实现或部分实现了闭包 ...

  7. python 内嵌函数, 闭包, 函数装饰器

    一.  函数内嵌 闭包 在python中,函数可以作为返回值, 可以给变量赋值. 在python中, 内置函数必须被显示的调用, 否则不会执行. #!/usr/bin/env python #-*- ...

  8. python函数知识七 闭包、装饰器一(入门)、装饰器二(进阶)

    21.闭包 闭包:在嵌套函数内,使用非全局变量(且不使用本层变量) 闭包的作用:1.保证数据的安全性(纯洁度).2.装饰器使用 .__closure__判断是否是闭包 def func(): a = ...

  9. guxh的python笔记三:装饰器

    1,函数作用域 这种情况可以顺利执行: total = 0 def run(): print(total) 这种情况会报错: total = 0 def run(): print(total) tot ...

  10. Python编程四大神兽:迭代器、生成器、闭包和装饰器

    生成器 生成器是生成一个值的特殊函数,它具有这样一个特点:第一次执行该函数时,先从头按顺序执行,在碰到yield关键字时该函数会暂停执行该函数后续的代码,并且返回一个值:在下一次调用该函数执行时,程序 ...

随机推荐

  1. Ds100p -「数据结构百题」31~40

    31.P2163 [SHOI2007]园丁的烦恼] 很久很久以前,在遥远的大陆上有一个美丽的国家.统治着这个美丽国家的国王是一个园艺爱好者,在他的皇家花园里种植着各种奇花异草. 有一天国王漫步在花园里 ...

  2. 自己把源码生成jar,在android项目中调用

    项目源码下载地址 看了很多,找了很多,都是没有自己想要的效果的,不容易啊,备注下吧. 1.自己的源码 ,java文件,里边有各种方法,生成jar,可以分享给别人使用. 2.目前主要验证在android ...

  3. 2020/5/8—cf,我裂开来

    呜呜呜我爆零了呜呜呜ljll 嗯T1T2防爆零的没了呜呜呜在此纪念可怜的yjz大佬21发AC 太惨了(逃 先来说说我们都有些啥题目吧... T1 嗯,裂开了,当场裂开我一看!桶排!然后实现,嗯?嗯!嗯 ...

  4. Trino容错模式深度测评与思考

    本文分享自华为云社区<走向批处理-交互式分析一体化: Trino容错模式深度测评与思考>,作者:HetuEngine九级代言 . 本文系华为云大数据研发团队原创,原创作者:文博,梦月 1 ...

  5. 算法修养--A*寻路算法

    A*寻路算法 广度优先算法 广度优先算法搜索以广度做未优先级进行搜索. 从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点. 这种算法就像洪水(Flo ...

  6. Go包介绍与初始化:搞清Go程序的执行次序

    Go包介绍与初始化:搞清Go程序的执行次序 目录 Go包介绍与初始化:搞清Go程序的执行次序 一.main.main 函数:Go 应用的入口函数 1.1 main.main 函数 1.2 main.m ...

  7. 基于LangChain的LLM应用开发1——介绍

    这是基于LangChain的大语言模型应用开发系列的第一篇. 文章内容会参考deeplearning.ai的短课程(https://learn.deeplearning.ai/langchain/), ...

  8. Linux账号密码安全运维

    前言 随着云计算厂商的兴起,云资源如ECS不再只有企业或者公司才会使用,普通人也可以自己买一台ECS来搭建自己的应用或者网站.虽然云计算厂商帮我们做了很多安全相关的工作,但并不代表我们的机器资源就绝对 ...

  9. 详解GuassDB数据库权限命令:GRANT和REVOKE

    本文分享自华为云社区<GuassDB数据库的GRANT & REVOKE>,作者: Gauss松鼠会小助手2 . 一.GaussDB的权限概述 在数据库中,对象的创建者将成为该对象 ...

  10. C#.NET 国密SM4 CBC 对称加解密 与JAVA互通 ver:20231103

    C#.NET 国密SM4 CBC 对称加解密 与JAVA互通 ver:20231103 .NET 环境:.NET6 控制台程序(.net core). JAVA 环境:JAVA8,带maven 的JA ...