7.函数用法

1 为什么需要函数

在 Python 实际开发中,我们使用函数的目的只有一个“让我们的代码可以被重复使用”。

函数的作用有两个:

  1. 模块化编程
  2. 代码复用
> 在编程领域,编程可以分为两大类:
1. 模块化编程
> 2. 面向对象编程

2 什么是函数

所谓的函数就是一个被命名的、独立的、完成特定功能的代码段(一段连续的代码),并可能给调用它的程序一个返回值

  1. 一个程序由一个个任务组成;函数就是代表一个任务或者一个功能。
  2. 函数是代码复用的通用机制。

函数是可重用的程序代码块。函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。

在编写函数时,函数体中的代码写法和我们前面讲述的基本一致,只是对代码实现了封装,并增加了函数调用、传递参数、返回计算结果等内容。

未命名的:在 Python 中,函数大多数是有名函数。当然 Python 中也存在没有名字的函数叫做匿名函数

独立的、完成特定功能的代码段:在实际项目开发中,定义函数前一定要先思考一下,这个函数是为了完成某个操作或某个功能而定义的(函数的功能一定要专一)。

返回值:很多函数在执行完毕后,会通过 return 关键字返回一个结果给调用它的位置。

3 Python 函数的分类

images/7.函数用法/Pasted-image-20241214215613.png

Python 中函数分为如下几类:

  1. 内置函数
    我们前面使用的 str()list()len() 等这些都是内置函数,我们可以拿来直接使用。
  2. 标准库函数
    我们可以通过 import 语句导入库,然后使用其中定义的函数。
  3. 第三方库函数
    Python 社区也提供了很多高质量的库。下载安装这些库后,也是通过 import 语句导入,然后可以使用这些第三方库的函数。
  4. 用户自定义函数
    用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。

4 函数的定义

基本语法:

def 函数名称([参数1, 参数2, ...]):
    '''文档字符串'''
    函数体
    ...
    [return 返回值]

要点:

  1. 我们使用 def 来定义函数,然后就是一个空格和函数名称;
    • Python 执行 def 时,会创建一个函数对象,并绑定到函数名变量上。
      images/7.函数用法/Pasted-image-20241214221421.png
  2. 参数列表
    • 圆括号内是形式参数列表,有多个参数时使用逗号隔开。
    • 定义时形式参数不需要声明类型,函数返回值也不需要声明类型
    • 无参数时,也必须保留空的圆括号。
    • 调用时传递的实参必须与形参列表一一对应。
  3. return 返回值
    • 如果函数体中包含 return 语句,则结束函数执行并返回值。
    • 如果函数体中不包含 return 语句,则返回 None 值。
  4. 调用函数之前,必须要先定义函数,即先调用 def 创建函数对象
    • 内置函数对象会自动创建。
    • 标准库和第三方库函数,通过 import 导入模块时,会执行模块中的 def 语句。

5 函数的调用

在 Python 中,函数和变量一样,都是先定义后使用。

# 定义函数
def 函数名称([参数1, 参数2, ...]):
    函数体
    ...
    [return 返回值]

# 调用函数
函数名称(参数1, 参数2, ...)

6 通过一个栗子引入函数

  1. 使用 Python 代码,编写一个打招呼程序。

    第一步:见到一个老师,打一声招呼
    print('您好')
    第二步:见到一个老师,打一声招呼
    print('您好')
    第二步:见到一个老师,打一声招呼
    print('您好')
    

    虽然以上程序可以满足程序的需求,但是我们发现,我们的代码做了很多重复性的工作。我们能不能对以上代码进行进一步的优化,避免代码的重复性编写。

  2. 升级:使用 Python 代码,编写一个打招呼程序(函数——一次编写,多次利用)。

    # 定义函数(封装函数)
    def greet():
        print('您好')
    
    
    # 调用函数
    # 见到一个老师,打一声招呼
    greet()
    # 见到一个老师,打一声招呼
    greet()
    # 见到一个老师,打一声招呼
    greet()
    

    images/7.函数用法/Pasted-image-20250928223844.png

  3. 升级:使用 Python 代码编写一个打招呼程序,可以实现向不同的人打不同的招呼。

# 定义一个函数,同时为其定义一个参数
def greet(name):
    print(f'{name},您好')


# 调用函数
# 见到了张老师,打一声招呼
greet('老张')
# 见到了李老师,打一声招呼
greet('老李')
# 见到了王老师,打一声招呼
greet('老王')

images/7.函数用法/Pasted-image-20250928223934.png

  1. 函数的设计原则“高内聚、低耦合”,函数执行完毕后,应该主动把结果返回给调用处,而不应该都交由 print() 等函数直接输出。
# 定义一个函数,拥有 name 参数,同时函数执行完毕后,拥有一个 return 返回值
def greet(name):
    # 执行一系列相关操作
    return name + ',您好'


# 调用函数
# 见到了张老师,打一声招呼
print(greet('老张'))  # 老张,您好
# 见到了李老师,打一声招呼
print("\033[0;31;40m\t" + greet('老李') + "\033[0m")
# 见到了王老师,打一声招呼
print("\033[0;36;40m\t" + greet('老王') + "\033[0m")
> 终端颜色参考:
![images/7.函数用法/Pasted-image-20250928224223.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010233-530701641.png)

7 聊聊 return 返回值

return 返回值要点:

  1. 如果函数体中包含 return 语句,则结束函数执行并返回值。
  2. 如果函数体中不包含 return 语句,则返回 None 值。
  3. 要返回多个返回值,使用列表、元组、字典、集合将多个值“存起来”即可。
思考
如果一个函数有两个 `return`(如下所示),程序如何执行?
> > ```python > def return_num(): > return "我是第一个 return" > return "我是第二个 return" > > > result = return_num() > print(result) # 1 > ```
只执行了第一个 `return`,原因是因为 `return` 可以退出当前函数,导致 `return` 下方的代码不执行。
> > ![images/7.函数用法/Pasted-image-20250325133023.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010275-213395476.png)
思考 2
如果一个函数要有多个返回值,该如何书写代码?

在 Python 中,理论上一个函数只能返回一个结果。但是如果我们想让一个函数同时返回多个结果,我们可以使用 `return 元组` 的形式。
> > ```python > def return_num(): > return 1, 2 > > > result = return_num() > print(result) > print(type(result)) # > ``` > > ![images/7.函数用法/Pasted-image-20250325132905.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010055-208086465.png)
思考 3
封装一个函数,参数有两个 num1,num2,求两个数的四则运算结果。
> 四则运算:加、减、乘、除
> ```python > def size(num1, num2): > jia = num1 + num2 > jian = num1 - num2 > cheng = num1 * num2 > chu = num1 / num2 > return jia, jian, cheng, chu > > > # 调用size方法 > print(size(20, 5)) > ``` > > ![images/7.函数用法/Pasted-image-20250325133535.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010254-13041457.png)

8 函数的应用案例

案例 1:使用 print 方法打印一条横线。

print('-' * 40)

案例 2:对案例 1 进行升级,可以根据输入的 num 数值,生成指定数量的横线。

def print_lines(num, length):
    """
    print_lines 函数主要作用用于生成多条指定长度的横线,
    拥有两个参数 num 和 length,都是 int 整型数据,
    num 用于控制生成的横线数量,
    length 用于控制生成横线的长度。
    """
    for i in range(num):
        print('-' * length)


# 调用函数
help(print_lines)
print_lines(4, 40)

images/7.函数用法/Pasted-image-20250928225256.png

案例 3:封装一个函数,用于求 3 个数的平均值。

def average_num(num1, num2, num3):
    """
    average_num 函数主要用于生成 3 个数的平均值,
    一共有3个参数 num1、num2、num3,
    要求是整型或浮点类型的数据,其返回结果就是三个数的平均值
    """
    res = num1 + num2 + num3
    # 求平均值
    return res / 3


# help(average_num)
# 调用 average_num 方法
print(average_num(10, 20, 30))

images/7.函数用法/Pasted-image-20250928225435.png

练习题:编写一个函数,有一个参数 str1,输入信息如 "1.2.3.4.5",使用函数对齐进行处理,要求最终的返回结果为 "5-4-3-2-1"

def func(str1):
    # 方法一:对字符串进行翻转操作(切片)
    return str1[::-1].replace('.', '-')

    # 方法二:使用 split 切割,然后 reverse 进行翻转
    # list1 = str1.split('.')
    # list1.reverse()
    # return '-'.join(list1)


# 调用函数实现字符串翻转拼接
str1 = "1.2.3.4.5"
print(func(str1))  # 5-4-3-2-1

images/7.函数用法/Pasted-image-20250928225643.png

9 Python 函数中的说明文档

9.1 什么是说明文档

思考
定义一个函数后,程序员如何书写程序能够快速提示这个函数的作用?

查看函数注释。
思考
如果代码多,我们是不是需要在很多代码中找到这个函数定义的位置才能看到注释?如果想更方便的查看函数的作用怎么办?

调用**函数的说明文档(函数的说明文档也叫函数的文档说明)。**

9.2 定义函数的说明文档

  1. 定义函数的说明文档。

    # 1、定义一个 menu 菜单函数
    def menu():
        pass
    
    
    # 2、定义通讯录增加操作方法
    def add_student():
        """
        函数的说明文档:add_student 方法不需要传递任何参数,
        其功能就是实现对通讯录的增加操作
        """
        pass
    
    
    # 3、定义通讯录删除操作方法
    def del_student():
        pass
    
    
    # 4、定义通讯录修改操作方法
    def modify_student():
        pass
    
    
    # 5、定义通讯录查询操作方法
    def find_student():
        pass
    
  2. 调用函数的说明文档。

    help(函数名称)
    # 或
    函数名称.__doc__
    

    案例:调用 add_student() 方法。

    help(add_student)
    print("======或======")
    # 或
    print(add_student.__doc__)
    

    运行结果:

    images/7.函数用法/Pasted-image-20250928230147.png

9.3 封装一个函数,用于生成指定长度的验证码

import random


# 定义一个 generate_code() 函数
def generate_code(num):
    """
    generate_code 方法主要用于生成指定长度的验证码,
    有一个 num 参数,需要传递一个 int 类型的数值,
    其 return 返回结果为 num 长度的随机验证码
    """
    # 第一步:定义一个字符串
    str1 = "23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
    # 第二步:循环 num 次,代表生成 num 长度的字符串
    code = ''
    code_temp = []
    for _ in range(num):
        # 第三步:从字符串中随机抽取 num 个字符
        index = random.randint(0, len(str1) - 1)
        code_temp.append(str1[index])
    # 合成验证码
    code = "".join(code_temp)
    # 第四步:使用 return 返回验证码
    return code


# 求帮助(查看 generate_code 函数的作用以及需要传递的参数)
help(generate_code)

# 调用函数
print(generate_code(6))

images/7.函数用法/Pasted-image-20250928230341.png

10 函数也是对象,内存底层分析

Python 中,一切都是对象。实际上,执行 def 定义函数后,系统就创建了相应的函数对象。我们执行如下程序,然后进行解释:

def print_star(n):
    print("*" * n)


print(print_star)
print(id(print_star))

c = print_star
c(3)

images/7.函数用法/Pasted-image-20250928230445.png

10.1 代码分析

  1. 上面代码执行 def 时,系统中会创建函数对象,并通过 print_star 这个变量进行引用:

    images/7.函数用法/image-20220630152525891.png

  2. 我们执行 c = print_star 后,将 print_star 变量的值赋给了变量 c,内存图变成了:

    images/7.函数用法/image-20220630153657448.png

  3. 显然,我们可以看出变量 cprint_star 都是指向了同一个函数对象。因此,执行 c(3) 和执行 print_star(3) 的效果是完全一致的。

  4. Python 中,圆括号 () 意味着调用函数。在没有圆括号的情况下,Python 会把函数当做普通对象。

与此核心原理类似,我们也可以做如下操作:

zhengshu = int

zhengshu("234")

显然,我们将内置函数对象 int 赋值给了变量 zhengshu,这样 zhengshuint 都是指向了同一个内置函数对象。当然,此处仅限于原理性讲解,实际开发中没必要这么做。

11 变量的作用域

11.1 什么是变量的作用域

变量作用域指的是变量的作用范围(变量在哪里可用,在哪里不可用),不同作用域内同名变量之间互不影响。主要分为两类:局部变量全局变量

11.2 局部变量与全局变量

在 Python 中:

  • 定义在函数外部的变量就称之为全局变量
  • 定义在函数内部变量就称之为局部变量
# 定义在函数外部的变量(全局变量)
num = 10


# 定义一个函数
def func():
    # 函数体代码
    # 定义在函数内部的变量(局部变量)
    i = 1

11.3 变量作用域的作用范围

变量起作用的范围称为变量的作用域,不同作用域内同名变量之间互不影响。变量分为:全局变量、局部变量

全局变量:

  1. 在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
  2. 在整个程序运行环境中都可见。
  3. 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
  4. 全局变量一般做常量使用。
  5. 函数内要改变全局变量的值,要使用 global 声明一下。

局部变量:

  1. 在函数体中(包含形式参数)声明的变量。
  2. 局部变量的引用比全局变量快,优先考虑使用。
  3. 如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量。
  4. 在函数、类等内部可见。
  5. 局部作用域中的变量称为局部变量,其使用范围不能超过其所在局部作用域。
  6. 也称为本地作用域 local

全局变量:在整个程序范围内都可以直接使用。

str1 = 'hello'


# 定义一个函数
def func():
    # 在函数内部调用全局变量 str1
    print(f'在局部作用域中调用 str1 变量:{str1}')


# 直接调用全局变量 str1
print(f'在全局作用域中调用 str1 变量:{str1}')
# 调用 func 函数
func()

images/7.函数用法/Pasted-image-20250928232540.png

局部变量:在函数的调用过程中,开始定义,函数运行过程中生效,函数执行完毕后,销毁。

# 定义一个函数
def func():
    # 在函数内部定义一个局部变量
    num = 10
    print(f'在局部作用域中调用 num 局部变量:{num}')


# 调用 func 函数
func()
# 在全局作用域中调用 num 局部变量
print(f'在全局作用域中调用 num 局部变量:{num}')

运行结果:

images/7.函数用法/Pasted-image-20250928232723.png

  • 一般来讲外部作用域变量可以在函数内部可见,可以使用。
  • 反过来,函数内部的局部变量,不能在函数外部看到。

11.4 一个赋值语句的错误 ★★★

看下例 1(解释在后面):

x = 5


def foo():
    y = x + 1  # 报错吗
    # x += 1  # 打开这句报错吗,换成 x = 1 可以吗
    print(x)


foo()

images/7.函数用法/Pasted-image-20250928233014.png

x = 5


def foo():
    y = x + 1  # 报错吗
    x += 1  # 打开这句报错吗,换成 x = 1 可以吗
    print(x)


foo()

images/7.函数用法/Pasted-image-20250928233050.png

思考
为什么会报错,为什么报 `UnboundLocalError`?
x = 300


def foo():
    x += 1


foo()

images/7.函数用法/Pasted-image-20250928233818.png

原因分析:

  • x += 1 其实是 x = x + 1
  • 只要有 x = 出现,这就是赋值语句。相当于在 foo 函数内部定义一个局部变量 x ,那么 foo 函数内部所有变量 x 就都是这个局部变量 x 了。
  • x = x + 1 相当于使用了局部变量 x ,但是这个 x 变量还没有完成赋值,就被右边拿来做加 1 操作了。

上面的例 1 也可以类比解释:

x = 300


def foo():
    y = x + 1  # local variable 'x' referenced before assignment
    print(y)
    x += 1  # 赋值即定义,x 就是当前作用域的变量
    print(x)

# 只要在函数中出现 x= 变量赋值语句,且此变量不加任何语句的修饰
# 那么此变量就一定是当前函数的局部变量
# 在此函数中,所有的 x 都是使用该 x

x += 1 相当于在 foo 函数中定义了一个局部变量 x,则在 foo 函数的整个作用域中都优先使用该局部变量,而 y = x + 1 在 x 变量定义语句之前就调用 x 所以报本地变量 x 在赋值前调用的错误。

11.5 global 关键字

思考
如果有一个数据,在函数 A 和函数 B 中都要使用,该怎么办?

将这个数据存储在一个全局变量里面。

案例:如果把通讯录管理系统更改为模块化编程模式(程序 => 函数),面临问题:

# 定义全局变量
info = []


# 定义 funcA 函数
def funcA():
    # 向 info 全局变量中添加数据
    info.extend(["小龙女", "赵敏", "周芷若"])


# 定义 funcB 函数
def funcB():
    # 共享全局作用域中的全局变量 info
    for i in info:
        print(i)


funcA()
funcB()

images/7.函数用法/Pasted-image-20250928235819.png

这会产生一个问题:我们能不能在局部作用域中对全局变量进行修改呢?

# 定义全局变量 num = 10
num = 10


# 定义一个函数 func
def func():
    # 尝试在局部作用域中修改全局变量
    num = 20
    print(f'在函数中 num 为 {num}')


# 调用函数 func
func()
# 尝试访问全局变量 num
print(f'在函数外 num 为 {num}')

images/7.函数用法/Pasted-image-20250928235419.png

最终结果:弹出 10,所以由运行结果可知,在函数体内部理论上是没有办法对全局变量进行修改的,所以要修改全局变量,必须使用 global 关键字声明。

# 定义全局变量 num = 10
num = 10


# 定义一个函数 func
def func():
    # 尝试在局部作用域中修改全局变量
    global num
    num = 20
    print(f'在函数中 num 为 {num}')


# 调用函数 func
func()
# 尝试访问全局变量 num
print(f'在函数外 num 为 {num}')

images/7.函数用法/Pasted-image-20250929000006.png

x = 5


def foo():
    global x  # 全局变量
    x += 1
    print(f'在函数内 x 为 {x}')


foo()
print(f'在函数外 x 为 {x}')

images/7.函数用法/Pasted-image-20250929000422.png

  • 使用 global 关键字声明,将 foo 内的变量 x 声明为外部的全局作用域中的变量 x。
  • 全局作用域中必须有 x 的定义。
思考
如果全局作用域中没有 x 定义会怎样?

注意
下面试验如果在 ipython、jupyter 中做,上下文运行环境中有可能有 x 的定义,稍微不注意,就测试不出效果。
# 有错吗?
def foo():
    global x
    x = x + 1
    print(x)


# 没有在全局中声明全局变量 x
foo()

images/7.函数用法/Pasted-image-20250929000729.png

# 有错吗?
def foo():
    global x
    x = 10
    x = x + 1
    print(f"foo 函数中 x 为 {x}")


foo()
print(f"foo 函数外 x 为 {x}")  # 可以吗?

images/7.函数用法/Pasted-image-20250929000903.png

使用 global 关键字定义的变量,虽然是在 foo 函数中声明的,但是该变量的作用域并不是 foo 函数,而是外部的全局作用域;即使是在 foo 函数中又写了 x = 10,也不会在 foo 这个局部作用域中定义局部变量 x,而是将其定义为全局变量。

注意
使用了 `global`,foo 中的 x 就不再是局部变量了,它是全局变量。

global 使用原则

  • 外部作用域变量在内部作用域可见,但也不要在这个内部的局部作用域中直接使用,因为函数的目的就是为了封装,尽量与外界隔离。
  • 如果函数需要使用外部全局变量,请尽量使用函数的形参定义,并在调用时传实参的方式解决
  • 一句话:不用 global。学习它就是为了深入理解变量作用域。

11.6 多函数之间数据的共享

# 定义全局变量
info = []


# 定义 funcA 函数:向全局变量中添加信息
def funcA():
    # 使用 global 声明全局变量
    global info
    # 向 info 全局变量中添加数据
    info.append({...})


# 定义 funcB 函数:查询功能,需要共享全局作用域中的通讯录信息
def funcB():
    # 共享全局作用域中的全局变量 info
    for i in info:
        ...

11.7 把函数的返回值作为另外一个函数的参数

def test1():
    return 50


def test2(num):
    print(num)


# 1. 保存函数 test1 的返回值
result = test1()

# 2.将函数返回值所在变量作为参数传递到 test2 函数
test2(result)  # 50

images/7.函数用法/Pasted-image-20250929001356.png

11.8 局部变量和全局变量效率测试

局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是在循环的时候。

在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量提高运行速度。

# 测试局部变量、全局变量的效率
import math
import time


def test01():
    start = time.time()
    for i in range(10000000):
        math.sqrt(30)
    end = time.time()
    print("全局变量耗时{0}".format((end - start)))


def test02():
    # 将全局变量 math.sqrt 转为局部变量 b
    b = math.sqrt
    start = time.time()
    for i in range(10000000):
        b(30)
    end = time.time()
    print("局部变量耗时{0}".format((end - start)))


test01()
test02()

images/7.函数用法/Pasted-image-20250929001530.png

12 函数的参数进阶

12.1 函数的参数

在函数定义与调用时,我们可以根据自己的需求来实现参数的传递。在 Python 中,函数的参数一共有两种形式:形参 与 实参。

  • 形参:在函数定义时,所编写的参数就称之为形式参数。
  • 实参:在函数调用时,所传递的参数就称之为实际参数。
# name 就是在函数 greet 定义时,所编写的参数(形参)
def greet(name):
    return name + ',您好'


# 调用函数
name = '老王'
# 在函数调用时,所传递的参数就是实际参数
greet(name)
注意:
虽然我们在函数传递时,喜欢使用相同的名称作为参数名称。但是两者的作用范围是不同的。`name = '老王'` ,代表实参,其是一个全局变量;而 `greet(name)` 函数中的 name 实际是在函数定义时才声明的变量,所以其是一个局部变量。

12.2 参数的传递

函数的参数传递本质上就是:从实参到形参的赋值操作。Python 中一切皆对象,所有的赋值操作都是引用的赋值。所以,Python 中参数的传递都是引用传递,不是值传递

具体操作时分为两类:

  1. 可变对象进行写操作,直接作用于原对象本身
  2. 不可变对象进行写操作,会产生一个新的对象空间,并用新的值填充这块空间(起到其他语言的“值传递”效果,但不是“值传递”)。
  • 可变对象有:
    字典、列表、集合、自定义的对象等。
  • 不可变对象有:
    数字、字符串、元组、function 等。

传递可变对象的引用

传递参数是可变对象(例如:列表、字典、自定义的其他可变对象等),实际传递的还是对象的引用。在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。

案例:参数传递:传递可变对象的引用。

b = [10, 20]


def func(m):
    # b 和 m 是同一个对象
    print("m 变量的地址:", id(m))
    # 由于 m 是可变对象,不创建对象拷贝,直接修改这个对象
    m.append(30)


print(f"调用 func 前 b 的值为:{b}")
print("b 变量的地址:", id(b))
func(b)
print(f"调用 func 后 b 的值为:{b}")

images/7.函数用法/Pasted-image-20250929224454.png

传递不可变对象的引用

传递参数是不可变对象(例如:intfloat、字符串、元组、布尔值),实际传递的还是对象的引用。在赋值操作时,由于不可变对象无法修改,系统会新创建一个对象。

a = 100


def func(n):
    # 传递进来的是 a 对象的地址
    print("func 中修改前 n 变量的地址为", id(n))
    # 由于 a 是不可变对象,因此创建新的对象 n
    n = n + 200
    # n 已经变成了新的对象
    print("func 中修改后 n 变量的地址为", id(n))
    print(n)


print("调用 func 前 a 变量的地址为", id(a))
func(a)
print("调用 func 后 a 变量的地址为", id(a))

images/7.函数用法/Pasted-image-20250929224840.png

显然,通过 id 值我们可以看到 变量 n 和变量 a 一开始是同一个对象。修改变量 n 的值后,会创建新的对象赋值哥变量 n

浅拷贝和深拷贝

为了更深入的了解参数传递的底层原理,我们需要讲解一下浅拷贝和深拷贝。我们可以使用内置函数:浅拷贝:copy、深拷贝:deepcopy

浅拷贝:不拷贝子对象的内容,只是拷贝子对象的引用

深拷贝:会连子对象的内容也全部拷贝一份,对子对象的修改不会影响源对象。

> Python 中大多数复制,一般使用**浅拷贝**。
```python """测试浅拷贝和深拷贝""" import copy

def test_copy():
'''测试浅拷贝'''
a = [10, 20, [5, 6]]
b = copy.copy(a)

print("a:", a)
print("b:", b)
b.append(30)
b[2].append(7)
print("浅拷贝")
print("a:", a)
print("b:", b)

def test_deepcopy():
'''测试深拷贝'''
a = [10, 20, [5, 6]]
b = copy.deepcopy(a)

print("a:", a)
print("b:", b)
b.append(30)
b[2].append(7)
print("深拷贝")
print("a:", a)
print("b:", b)

print("浅拷贝")
test_copy()
print("深拷贝")
test_deepcopy()


![images/7.函数用法/Pasted-image-20250929225945.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010831-1686790807.png)

内存图:

![images/7.函数用法/image-20220630174737096.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010695-97273248.png)

![images/7.函数用法/image-20220630185724916.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010785-1777818303.png)

#### 传递不可变对象包含的子对象是可变的情况

传递不可变对象时。不可变对象里面包含的子对象是可变的。则方法内修改了这个可变对象,原对象也发生了变化。

```python
a = (10, 20, [5, 6])


def test(m):
    print(f"函数中修改前 m 地址为:{id(m)}")
    m[2][0] = 888
    print(f"函数中修改后 m 地址为:{id(m)}")
    print(f"函数中修改后 m 值:{m}")


print(f"调用函数前 a 的地址:{id(a)}")
print(f"调用函数前 a 的值:{a}")
print("=" * 40)
test(a)
print("=" * 40)
print(f"调用函数后 a 的地址:{id(a)}")
print(f"调用函数后 a 的值:{a}")

images/7.函数用法/Pasted-image-20251001165808.png

12.3 函数的参数类型

images/7.函数用法/Pasted-image-20241214232719.png

☆ 位置参数

理论上,在函数定义时,我们可以为其定义多个参数。但是在函数调用时,我们也应该传递多个参数,正常情况,其要一一对应。

def user_info(name, age, address):
    print(f'我的名字{name},今年{age}岁了,家里住在{address}')


# 调用函数
user_info('Tom', 23, '美国纽约')

images/7.函数用法/Pasted-image-20251001170247.png

注意
位置参数强调的是参数传递的位置必须一一对应,不能颠倒。

☆ 关键字参数(Python 特有)

函数调用,通过 键=值 形式加以指定。可以让函数更加清晰、容易使用,同时也清除了参数的顺序需求。按照形参的名称传递参数,称为命名参数,也称关键字参数

def user_info(name, age, address):
    print(f'我的名字{name},今年{age}岁了,家里住在{address}')


# 调用函数(使用关键词参数)
user_info(age=23, address='美国纽约', name='Tom')

images/7.函数用法/Pasted-image-20251001170247.png

☆ 函数定义时缺省参数(参数默认值)

缺省参数也叫默认参数,用于定义函数,为参数提供默认值,调用函数时可不传该默认参数的值(注意:所有位置参数必须出现在默认参数前,包括函数定义和调用)。

def user_info(name, age, gender='男'):
    print(f'我的名字{name},今年{age}岁了,我的性别为{gender}')


user_info('李林', 25)
user_info('振华', 28)
user_info('婉儿', 18, '女')

images/7.函数用法/Pasted-image-20251001170930.png

注意
我们在定义缺省参数时,一定要把其写在参数列表的最后侧。

☆ 不定长参数

不定长参数也叫可变参数。用于不确定调用的时候会传递多少个参数(不传参也可以)的场景。此时,可用包裹(packing)位置参数(可变位置参数)或者包裹关键字参数(可变关键字参数),来进行参数传递,会显得非常方便。

可变参数指的是可变数量的参数。分两种情况:

  1. *param(一个星号),将多个参数收集到一个元组对象中。
  2. **param(两个星号),将多个参数收集到一个字典对象中。
☆ 包裹 (packing) 位置参数
def user_info(*args):
    # print(args)  # 元组类型数据,对传递参数有顺序要求
    print(f'我的名字{args[0]},今年{args[1]}岁了,住在{args[2]}')


# 调用函数,传递参数
user_info('Tom', 23, '美国纽约')

images/7.函数用法/Pasted-image-20251001170247.png

☆ 包裹关键字参数
def user_info(**kwargs):
    # print(kwargs)  # 字典类型数据,对传递参数没有顺序要求,格式要求 key = value 值
    print(f'我的名字{kwargs["name"]},今年{kwargs["age"]}岁了,住在{kwargs["address"]}')


# 调用函数,传递参数
user_info(name='Tom', address='美国纽约', age=23)

images/7.函数用法/Pasted-image-20251001170247.png

> kwargs = keyword + args
综上:无论是包裹位置传递还是包裹关键字传递,都是一个组包的过程。
Python 组包
就是把多个数据组成元组或者字典的过程。
总结
  • 可变参数包括可变位置参数可变关键字参数
  • 可变位置参数在形参前使用一个星号 *
  • 可变关键字参数在形参前使用两个星号 **
  • 可变位置参数和可变关键字参数都可以收集若干个实参,可变位置参数收集形成一个 tuple 类型对象,可变关键字参数收集形成一个 dict 类型对象。
  • 混合使用参数的时候,普通参数需要放到参数列表前面,可变参数要放到参数列表的后面,可变位置参数需要在可变关键字参数之前。

☆ 强制命名参数(keyword-only)

在 Python3.x 之后,新增了 keyword-only 参数。

keyword-only 参数:在形参定义时,在一个 * 星号之后,或一个可变位置参数之后,出现的普通参数,就已经不是普通的参数了,称为 keyword-only 参数。

在带星号的可变参数后面增加新的参数叫强制命名参数,在调用的时候必须使用关键字传参

案例:强制命名参数的使用。

def f1(*a, b, c):
    print(a, b, c)


f1(2, b=3, c=4)

# 会报错。由于 a 是可变参数,将 2,3,4 全部收集。造成 b 和 c 没有赋值。
f1(2, 3, 4)

images/7.函数用法/Pasted-image-20251001173444.png

def fn(*args, x, y, **kwargs):
    print(x, y, args, kwargs, sep='\n', end='\n\n')


# 没有为强制命名参数 x、y 传参,3 和 5 都被 args 接收
fn(3, 5)
# 没有为强制命名参数 x、y 传参,3、5、7都被 args 接收
fn(3, 5, 7)
# 没有为强制命名参数 x、y 传参,3 和 5 被 args 接收,a=1,b='atc' 被 kwargs 接收
fn(3, 5, a=1, b='abc')
# 正确写法
fn(3, 5, y=6, x=7, a=1, b='atc')

keyword-only 参数另一种形式:

* 星号后所有的普通参数都成了 keyword-only 参数。

# 通过 * 号来创建强制命名参数,在 * 后的变量为强制命名参数,
def f1(*, b, c):
    print(b, c)


f1(1, 2)

images/7.函数用法/Pasted-image-20251001173820.png

# 通过 * 号来创建强制命名参数,在 * 后的变量为强制命名参数,
def f1(*, b, c):
    print(b, c)


f1(b=1)

images/7.函数用法/Pasted-image-20251001173851.png

# 通过 * 号来创建强制命名参数,在 * 后的变量为强制命名参数,
def f1(*, b, c):
    print(b, c)


f1(b=1, c=2)

images/7.函数用法/Pasted-image-20251001173912.png

☆ 强制位置参数(Positional-only)

Python3.8(2019 年 10 月发布 3.8.0)开始,增加了最后一种形参类型的定义:Positiona-only 参数。

定义形参列表时,/ 之前的参数都会变成强制位置参数。

def fn(a, /):
    print(a, sep='\n')


fn(3)
# fn(a=4)  #错误,仅位置参数,不可以使用关键字传参

images/7.函数用法/Pasted-image-20251001174054.png

def fn(a, /):
    print(a, sep='\n')


# fn(3)
fn(a=4)  #错误,仅位置参数,不可以使用关键字传参

images/7.函数用法/Pasted-image-20251001174113.png

☆ 参数的混合使用

# 可变位置参数、keyword-only 参数、缺省值
def fn(*args, x=5):
    print(x)
    print(args)
    print("-------------")


fn()  # 等价于 fn(x=5)
fn(5)  # 5 给了 args
fn(x=6)  # args 没有接收到参数,x=6
fn(1, 2, 3, x=10)  # 1,2,3 给了 args,x=10

images/7.函数用法/Pasted-image-20251001174341.png

# 普通参数、可变位置参数、keyword-only 参数、缺省值
def fn(y, *args, x=5):
    print('x={},y={}'.format(x, y))
    print(args)


fn()  # 缺失了位置参数 y
fn(5)  # y=5, x=5
fn(5, 6)  # y=5, args=(6,), x=5
fn(x=6)  # 缺失了位置参数 y
fn(1, 2, 3, x=10)  # y=1, args=(2, 3), x=10
fn(y=17, 2, 3, x=16)  # 位置传参位于了关键字传参之后
fn(1, 2, y=3, x=10)  # y 重复赋值,第一个位置参数 1 已经赋给了 y
fn(y=28, x=30)  # args=()
# 普通参数、缺省值、可变关键字参数
def fn(x=5, **kwargs):
    print('x={}'.format(x))
    print(kwargs)
    print('----------')


fn()
fn(3)
fn(x=6)
fn(y=3, x=18)
fn(3, y=10)
fn(y=3, z=28)

images/7.函数用法/Pasted-image-20251001175011.png

☆ 参数规则

参数列表参数一般顺序是:positional-only 参数 -> 普通参数 -> 缺省参数 -> 可变位置参数 -> keyword-only 参数(可带缺省值)-> 可变关键字参数

注意:

  • 代码应该易读易懂,而不是为难别人。
  • 请按照书写习惯定义函数参数。
# a 和 b 是仅位置参数,只能用在 Python3.8 以后
def fn(a, b, /, x, y, z=3, *args, m=4, n, **kwargs):
    print(a, b)
    print(x, y, z)
    print(m, n)
    print(args)
    print(kwargs)
    print('-' * 30)
def connect(host='localhost', user='admin', password='admin', port='3306', **kwargs):
    print(host, port)
    print(user, password)
    print(kwargs)


connect(db='cmdb')  # 参数的缺省值把最常用的缺省值都写好了
connect(host='192.168.1.123', db='cmdb')
connect(host='192.168.1.123', db='cmdb', password='mysql')

images/7.函数用法/Pasted-image-20251001175230.png

  • 定义最常用参数为普通参数,可不提供缺省值,必须由用户提供。注意这些参数的顺序,最常用的先定义
  • 将必须使用名称的才能使用的参数,定义为 keyword-only 参数,要求必须使用关键字传参。
  • 如果函数有很多参数,无法逐一定义,可使用可变参数。如果需要知道这些参数的意义,则使用可变关键字参数收集。

13 Python 拆包(元组和字典)

13.1 什么是拆包

Python 拆包:就是把元组或字典中的数据单独的拆分出来,然后赋予给其他的变量。

拆包:对于函数中的多个返回数据,去掉元组,列表或者字典直接获取里面数据的过程。

13.2 元组的拆包过程

def func():
    # 经过一系列操作返回一个元组
    return 100, 200  # tuple 元组类型的数据


# 定义两个变量接收元组中的每个数组(拆包)
num1, num2 = func()
# 打印 num1 和 num2
print(num1)
print(num2)

images/7.函数用法/Pasted-image-20251001175712.png

13.3 字典的拆包过程

记住:字典拆包,默认只能把每个元素的 key 拆出来。

dict1 = {'name': '小明', 'age': 18}
# 拆包的过程(字典)
a, b = dict1

print(a)
print(b)

# 获取字典中的数据
print(dict1[a])
print(dict1[b])

images/7.函数用法/Pasted-image-20251001175754.png

将字典的值拆出来:

dict1 = {'name': '小明', 'age': 18}
# 拆包的过程(字典)
a, b = dict1.values()

print(a)
print(b)

images/7.函数用法/Pasted-image-20251001175929.png

将字典的键值对拆出来:

dict1 = {'name': '小明', 'age': 18}
# 拆包的过程(字典)
a, b = dict1.items()

print(a)
print(b)

images/7.函数用法/Pasted-image-20251001180109.png

13.4 拆包的应用案例

案例 1:使用至少 3种方式 交换两个变量的值。

第一种方式:引入一个临时变量。

c1 = 10
c2 = 2

# 引入临时变量 temp
temp = c2
c2 = c1
c1 = temp

print(c1, c2)

images/7.函数用法/Pasted-image-20251001180145.png

第二种方式:使用加法与减法运算交换两个变量的值(不需要引入临时变量)。

c1 = 10
c2 = 2

c1 = c1 + c2
c2 = c1 - c2
c1 = c1 - c2

print(c1, c2)

images/7.函数用法/Pasted-image-20251001180145.png

images/7.函数用法/image-20210313171658120.png

第三种方法:只有 Python 才具有的特性,叫做拆包。

c1 = 10
c2 = 2

c1, c2 = c2, c1

images/7.函数用法/Pasted-image-20251001180145.png

原理:

  1. 把 c2 和 c1 组成一个元组 (c2,c1)
  2. 使用拆包特性,把元组中的两个元素分别赋值给 c1 和 c2。

案例 2:Python 中数据的传递案例。

def func(*args, **kwargs):
    print(args)
    print(kwargs)


# 定义一个元组(也可以是列表)
tuple1 = (10, 20, 30)
# 定义一个字典
dict1 = {'first': 40, 'second': 50, 'third': 60}

# 需求:把元组传递给 *args 参数,字典传递给 **kwargs
# 1. 如果想把元组传递给 *args,必须在 tuple1 的前面加一个 * 号
# 2. 如果想把字典传递给 **kwargs,必须在 dict1 的前面加两个 * 号
func(*tuple1, **dict1)

images/7.函数用法/Pasted-image-20251001180512.png

14 lambda 表达式和匿名函数

lambda 表达式可以用来声明匿名函数。lambda 函数是一种简单的、在同一行中定义函数的方法。lambda 函数实际生成了一个函数对象。

lambda 表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。

lambda 表达式的基本语法如下:

lambda arg1, arg2, arg3... : <表达式>

arg1, arg2, arg3 为函数的参数。<表达式> 相当于函数体。运算结果是:表达式的运算结果。

f = lambda a, b, c: a + b + c
print(f)
print(f(2, 3, 4))

g = [lambda a: a * 2, lambda b: b * 3, lambda c: c * 4]
print(g[0](6), g[1](7), g[2](8))

images/7.函数用法/Pasted-image-20251001180659.png

注意
lambda 中不能出现 `=` ,更不能出现 `return`。
  • 使用 lambda 关键字定义匿名函数,格式为 lambda[参数列表]: 表达式
  • 参数列表不需要小括号。无参就不写参数。
  • 冒号用来分割参数列表和表达式部分。
  • 不需要使用 return。表达式的值,就是匿名函数的返回值。表达式中不能出现等号。
  • lambda 表达式(匿名函数)只能写在一行上,也称为单行函数。

15 lambda 表达式相关应用

15.1 ☆ 带默认参数的 lambda 表达式

fn = lambda a, b, c=100: a + b + c

print(fn(10, 20))

images/7.函数用法/Pasted-image-20251001180949.png

15.2 ☆ 不定长参数:可变参数 *args

fn1 = lambda *args: args

print(fn1(10, 20, 30))

images/7.函数用法/Pasted-image-20251001181035.png

15.3 ☆ 不定长参数:可变参数 **kwargs

fn2 = lambda **kwargs: kwargs

print(fn2(name='Tom', age=20, address='北京市海淀区'))

images/7.函数用法/Pasted-image-20251001181056.png

15.4 ☆ 带 if 判断的 lambda 表达式

fn = lambda a, b: a if a > b else b

print(fn(10, 20))

images/7.函数用法/Pasted-image-20251001181750.png

15.5 ☆ 列表数据 + 字典数据排序(重点)

知识点:列表.sort(key=排序的 key 索引, reverse=False)

students = [
    {'name': 'Tom', 'age': 20},
    {'name': 'Rose', 'age': 19},
    {'name': 'Jack', 'age': 22}
]

# 按 name 值升序排列
students.sort(key=lambda x: x['name'])
print(students)

# 按 name 值降序排列
students.sort(key=lambda x: x['name'], reverse=True)
print(students)

# 按 age 值升序排列
students.sort(key=lambda x: x['age'])
print(students)

images/7.函数用法/Pasted-image-20250325161519.png

执行流程:

students = [
    {'name': 'Tom', 'age': 20},
    {'name': 'Rose', 'age': 19},
    {'name': 'Jack', 'age': 22}
]

# 按 name 值升序排列
students.sort(key=lambda x: x['name'])
print(students)

匿名函数往往用在为高阶函数传参时。使用 lambda 表达式,往往能简化代码。

# 返回常量的函数
print((lambda: 0)())

# 加法匿名函数,带缺省值
print((lambda x, y=3: x + y)(5))
print((lambda x, y=3: x + y)(5, 6))

# keyword-only 参数
print((lambda x, *, y=30: x + y)(5))
print((lambda x, *, y=30: x + y)(5, y=10))

# 可变参数
print((lambda *args: (x for x in args))(*range(5)))
print((lambda *args: [x + 1 for x in args])(*range(5)))
print((lambda *args: {x % 2 for x in args})(*range(5)))

# defaultdict
from collections import defaultdict

d = defaultdict(lambda: list())
d['a'].extend(range(5))
print(d)

# sorted
x = ['a', 1, 'b', 20, 'c', 32]
print(sorted(x, key=str))
# 如果按照数字排序怎么做?
x = ['a', 1, 'b', 20, 'c', 32]
print(sorted(x, key=lambda x: x if isinstance(x, int) else int(x, 16)))

16 eval() 函数

功能:将字符串 str 当成有效的表达式来求值并返回计算结果。

语法:

eval(source[, globals[, locals]])

参数:

  1. source:一个 Python 表达式或函数 compile() 返回的代码对象。
  2. globals:可选。必须是 dictionary
  3. locals:可选。任意映射对象。
# 测试 eval() 函数

s = "print('abcde')"
eval(s)

a = 10
b = 20

c = eval("a+b")
print(c)

dict1 = dict(a=100, b=200)

d = eval("a+b", dict1)
print(d)

images/7.函数用法/Pasted-image-20251001182825.png

注意
eval 函数会将字符串当做语句来执行,因此会有被注入的安全隐患。比如:字符串中含有删除文件的语句。那就麻烦大了。因此,使用时候,要慎重!!!

17 递归函数

17.1 前言

编程思想:如何利用数学模型,来解决对应的需求问题;然后利用代码实现对应的数据模型。

算法:使用代码实现对应的数学模型,从而解决对应的业务问题。

注意
程序 = 算法 + 数据结构

在我们经常使用的算法中,有两种非常常用的算法:递推算法 + 递归算法,专门用于解决一些比较复杂,但是拆分后相似度又非常高的程序。

注意
递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时,谨慎使用。

2、递推算法

递推算法:递推算法是一种简单的算法,即通过已知条件,利用特定条件得出中间推论,直至得到结果的算法。递推又分为顺推和逆推。

顺推:通过最简单的条件,然后逐步推演结果。

逆推:通过结果找到规律,然后推导已知条件。

递推算法案例:斐波那契数列。

1 1 2 3 5 8 13 21 …

① ② ③ ④ ⑤ ⑥ …

第 1 位为 1,第 2 位为 1,第 3 位为 2 = 1 + 1,第 4 位为 3 = 2 + 1,依次类推……第 n 位结果为多少?

f(n) = f(n-1) + f(n-2)

提出问题:求斐波那契数列第 15 位的结果?

分析:

f(15) = f(14) + f(13)

​f(14) = f(13) + f(12)

​f(13) = f(12) + f(11)

​…

​f(4) = f(3) + f(2) = 3 + 1

​f(3) = f(2) + f(1) = 2

​f(2) = 1

​f(1) = 1

递推算法:使用 while 循环或 for 循环。

# 递推算法:根据已知条件,求结果(或者根据结果求未知条件)
def recursive(n):
    """
    返回斐波那契数列某一位(n >= 1)的结果
    """
    # 设置结束条件
    if n == 1 or n == 2:
        return 1

    # 开始递推
    # f(3) = f(2) + f(1)
    # f(4) = f(3) + f(2)
    # ...
    # f(15) = f(14) + f(13)
    dict1 = {1: 1, 2: 1}

    for i in range(3, n + 1):
        # f(3) = f(2) + f(1)
        # f(i) = f(i-1) + f(i-2)
        dict1[i] = dict1[i - 1] + dict1[i - 2]

    return dict1[n]


# 函数调用
print(recursive(15))

images/7.函数用法/Pasted-image-20251001184125.png

17.2 什么是递归算法

程序调用自身的编程技巧称为递归(recursion)。递归做为一种算法在程序设计语言中广泛应用,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

  1. 简化问题:找到最优子问题(不能再小)。
  2. 函数自己调用自己。
def func():
    # 自己调用自己
    func()


func()

17.3 递归两种重要的元素

递归有两个非常重要的概念:

  1. 递归点:找到解决当前问题的等价函数(先解决规模比当前问题小一些的函数,依次类推,最终实现对问题的解决) => 有递有归。
  2. 递归出口:当问题解决的时候,已经到达(必须存在)最优问题,就不能再次调用函数了。
如果一个递归函数没有递归出口就变成了死循环。

17.4 编写递归三步走

  1. 明确你这个函数想要干什么

    如:求斐波那契数列。

  2. 寻找递归结束条件

    如:就是在什么情况下,递归会停止循环,返回结果。

  3. 找出函数的等价关系式

    如:斐波那契数列,第 n 位 f(n) = f(n-1) + f(n-2)

案例 1:使用递归求斐波那契数列。

第一步:明确这个函数想要干什么(先定义出来,明确调用方式)。

# 斐波那契数列 1 1 2 3 5 8 13 21 ...
def f(n):
    # 编写递归代码求第 n 位的结果
    pass


# 调用函数
print(f(15))  # 610

第二步:寻找递归的结束条件。

# 斐波那契数列 1 1 2 3 5 8 13 21 ...
def f(n):
    # 编写递归代码求第 n 位的结果
    if n == 1 or n == 2:
        return 1


# 调用函数
print(f(15))  # 610

第三步:找出函数的等价关系式(最关键的一步)。

# 斐波那契数列 1 1 2 3 5 8 13 21 ...
def f(n):
    # 编写递归代码求第 n 位的结果
    if n == 1 or n == 2:
        return 1

    # 找出与斐波那契数列等价的关系式
    return f(n - 1) + f(n - 2)


# 调用函数
print(f(15))  # 610

案例 2:使用递归求 N 的阶乘(如 n=100)。

阶乘是什么?一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积。

如:n! = 1 × 2 × 3 × … × (n-1) × n

1! = 1

2! = 1 x 2 = 2

3! = 1 x 2 x 3 = 6

4! = 1 x 2 x 3 x 4 = 24

n! = 1 × 2 × 3 × … × (n-1) × n

第一步:明确这个函数要做什么以及定义函数以及调用方式。

def f(n):
    # 编写递归条件
    pass


print(f(100))

第二步:寻找递归的结束条件。

def f(n):
    # 编写递归结束条件
    if n <= 2:
        return n
    # ...递归等式


print(f(100))

第三步:编写递归等价公式(自己要调用自己)。

等价公式 = 找规律

1! = f(1) = 1

2! = f(2) = 2

3! = f(2) x 3 = 6

4! = f(3) x 4 = 24

n! = f(n-1) * n

def f(n):
    # 编写递归结束条件
    if n <= 2:
        return n
    # ...递归等式
    return f(n - 1) * n


print(f(100))

案例 3:面试题 => 猴子吃桃问题

猴子吃桃问题。猴子第 1 天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第 2 天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半另加一个。到第 10 天早上想再吃时,就只剩下一个桃子了。求第 1 天共摘了多少个桃子?

第一步:确定函数主要要完成什么功能,需要传递哪些参数,确认调用方式。

def f(n):
    # 编写递归代码
    pass


# 调用 f 函数
print(f(1))

第二步:编写递归的结束条件(出口)。

# 第一步:确定函数功能
def f(n):
    # 第二步:编写递归结束条件(出口)
    if n == 10:
        return 1


# 调用函数
print(f(1))

第三步:找出与这个问题相等的等式关系。

求桃子的剩余数量?

第十天没吃时剩余:f(10) = 1
第九天没吃时剩余:f(9) = (f(10) + 1) * 2

第 n 天没吃时剩余:f(n) = (f(n + 1) + 1) * 2

第一天摘下:f(1) = (f(2) + 1) * 2

第 n 天,总剩余桃子的数量 = (第 (n+1) 天桃子的剩余桃子的数量 + 1) * 2

# 第一步:确定函数功能
def f(n):
    # 第二步:编写递归结束条件(出口)
    if n == 10:
        return 1
    # 第三步:寻找与这个问题相似的等价公式
    return (f(n + 1) + 1) * 2


# 调用函数
print(f(8))

18 函数的嵌套

18.1 什么是函数的嵌套

嵌套函数指的是在函数的内部再定义一个函数,但是需要注意的是,在函数内部定义的函数,无法被外部所访问,通俗来讲就是函数在哪里定义的就只能在同级别位置进行调用。

所谓函数嵌套调用指的是一个函数里面又调用了另外一个函数

一般在什么情况下使用嵌套函数?

  1. 封装——数据隐藏
    外部无法访问“嵌套函数”。
  2. 贯彻 DRY(Don’t Repeat Yourself)原则
    嵌套函数,可以让我们在函数内部避免重复代码。
  3. 闭包
    后面会详细讲解。

18.2 函数嵌套的基本语法

嵌套函数

案例:使用嵌套函数避免重复代码。

def print_chinese_name(name, family_name):
    print("{0} {1}".format(family_name, name))


def print_english_name(name, family_name):
    print("{0} {1}".format(name, family_name))


# 使用 1 个函数代替上面的两个函数
def print_name(is_chinese, name, family_name):
    def inner_print(a, b):
        print("{0} {1}".format(a, b))

    if is_chinese:
        inner_print(family_name, name)
    else:
        inner_print(name, family_name)


print_name(True, "小 七", "高")
print_name(False, "George", "Bush")

images/7.函数用法/Pasted-image-20251002215300.png

嵌套调用函数的执行流程

def funcB():
    print('这是 funcB 函数的函数体部分..·')


def funcA():
    print('-' * 40)
    print('这是 funcA 函数的函数体部分...')
    # 假设我们在调用 funcA 函数时,需要使用到 funcB 的相关功能,则可以嵌套到 funcA 方法中
    funcB()
    print('-' * 40)


# 调用 funcA 函数
funcA()

嵌套调用函数的执行流程:

  1. Python 代码遵循一个“顺序原则”,从上往下,从左往右一行一行执行。
    当代码执行到第 1 行时,则在计算机内存中定义一个 funcB 函数。但是其内部的代码并没有真正的执行,跳过第 2 行继续向下运行。

  2. 执行到第 5 行,发现又声明了一个 funcA 的函数,根据函数的定义原则,定义就是在内存中声明有这样一个函数,但是没有真正的调用和执行。

  3. 代码继续向下执行,到第 14 行,发现 funcA()函数名() 就代表调用 funcA 函数并执行其内部的代码。程序返回到第 6 行,然后一步一步向下执行,输出 40 个横杠,然后打印 "这是 funcA 函数的函数体部分…",然后继续向下执行,遇到 funcB 函数,后边有一个圆括号代表执行 funcB 函数,原程序处于等待状态。

  4. 进入 funcB 函数,执行输出 "这是 funcB 函数的函数体部分…",当代码完毕后,返回 funcA 函数中 funcB() 的位置,继续向下执行,打印 40 个横杠。

  5. 最终程序就执行结束了。

18.3 nonlocal 关键字

nonlocal:用来声明外层的局部变量。
global:用来声明全局变量。

案例:使用 nonlocal 声明外层局部变量。

# 测试 nonlocal、global 关键字的用法
a = 100


def outer():
    b = 10

    def inner():
        # 声明外部函数的局部变量
        nonlocal b
        print("inner b:", b)
        b = 20

        # 声明全局变量
        global a
        a = 1000

    inner()
    print("outer b:", b)


outer()
print("a:", a)

images/7.函数用法/Pasted-image-20251002220244.png

18.4 LEGB 规则

Python 在查找“名称”时,是按照 LEGB 规则查找的:

Local --> Enclosed --> Global --> Built in
  • Local:指的就是函数或者类的方法内部。本地作用域、局部作用域的 local 命名空间。函数调用时创建,调用结束消亡。
  • Enclosed:指的是嵌套函数(一个函数包裹另一个函数,例如:闭包),Python2.2 时引入了嵌套函数,实现了闭包。这个指的就是嵌套函数中外部函数的命名空间。
  • Global:指的是模块中的全局变量。全局作用域,即一个模块的命名空间。模块被 import 时创建,解释器退出时消亡。
  • Built in:指的是 Python 为自己保留的特殊名称。内置模块的命名空间,生命周期从 Python 解释器启动时创建到解释器退出时消亡。例如 print(open)printopen 都是内置的变量。

如果某个变量名(name)映射在局部命名空间(local)中没有找到,接下来就会在闭包作用域(enclosed)进行搜索,如果闭包作用域也没有找到,Python 就会到全局命名空间(global)中进行查找,最后还没有找到会在内建命名空间(built-in)中搜索 (如果一个名称在所有命名空间中都没有找到,就会产生一个 NameError)。

# 测试LEGB

str = "global"

def outer():
    str = "outer"

    def inner():
        str = "inner"
        print(str)

    inner()

outer()

我们依次将几个 str 注释掉,观察控制台打印的内容,体会 LEBG 的搜索顺序。

18.5 PyCharm 调试小技巧

Step over(F8):代码一步一步向下执行,但是遇到了函数以后,不进入函数体内部,直接返回函数的最终的执行结果。

Step into(F7):代码一步一步向下执行,但是遇到了函数以后,进入到函数体内部,一步一步向下执行,直到函数体的代码全部执行完毕。

images/7.函数用法/image-20210313120335429.png

images/7.函数用法/image-20210313121451376.png

19 闭包(closure) ★★★

images/7.函数用法/Pasted-image-20250329194604.png

根据字面意思,可以形象地把闭包理解为一个封闭的包裹,这个包裹就是一个函数。当然,还有函数内部对应的逻辑,包裹里面的东西就是自由变量(外部函数中的局部变量),自由变量可以随着包裹到处游荡。

  • 局部变量:如果名称绑定在一个代码块中,则为该代码块的局部变量,除非声明为 nonlocal 或 global。
  • 全局变量:如果名称绑定在模块层级,则为全局变量。
  • 自由变量未在本地作用域中定义的局部变量。例如定义在内层函数外的外层函数作用域中的局部变量

闭包:就是一个概念,出现在嵌套函数中,指的是内层函数引用到了外层函数的自由变量,就形成了闭包。很多语言都有这个概念,最熟悉就是 JavaScript。

闭包 = 函数 + 自由变量

闭包的特点

  1. 闭包是一个函数,而且存在于另一个函数当中。
  2. 闭包可以访问到父级函数的变量,且该变量不会销毁。

案例一:

# Python2 中闭包的实现
def counter():
    # 不是内层函数要用到自由变量
    x = 1
    c = [0]

    def inc():
        c[0] += 1  # 报错吗?为什么 # line8
        # x += 1  # line9
        # c += [1]  # line10
        return c[0]

    # 有意为之,就是要返回函数,
    # 函数用 foo 变量记住,c 闭包必须保留
    return inc  # 注意这一句返回什么?


foo = counter()  # line18
print(foo(), foo())  # lin19
c = 100
print(foo())  # line21
上面代码有几个问题
> 1. 第 8 行会报错吗?为什么? > > 2. 第 9 行放开会报错吗?为什么? > > 3. 第 10 行放开会报错吗?为什么? > > 4. 第 18 行会进行哪些操作? > > 5. 第 19 行打印什么结果? > > 6. 第 21 行打印什么结果? > > 7. 有闭包吗?
回答
> 1. 不报错,因为没有修改 `c` 的值,而是修改了 `c` 中元素的值。 > > 2. 会报错,因为没有使用 `nonlocal` 关键字声明 `x` 为外部函数的自由变量,所以 `x = x + 1` 中的 `x` 变量为本地局部变量,但是还没有对 `x` 变量做过赋值操作就进行使用,所以会报错。 > > 3. 会报错,因为对 `c` 进行了赋值操作,而没有使用 `nonlocal` 关键字声明 `c` 为自由变量,所以 `c` 时本地局部变量,但是还没有对 `c` 变量做过赋值操作就进行使用,所以会报错。 > > 4. 第 18 行会执行 counter 函数并返回 inc 对应的函数对象,注意这个函数对象并不释放,因为有 foo 记着。 > > 5. 打印:1,2 > > 6. 打印:3 > 第 20 行的 c 和 counter 中的 c 不一样,而 inc 引用的是自由变量,是 counter 中的变量 c。 > > 7. 有,`c[0]` 引用了外层函数的自由变量。

images/7.函数用法/Pasted-image-20251002224729.png

images/7.函数用法/image-20221007194247731.png

案例二:

tuple1 = (1, 2, [1, 2])


def outer():
    tuple2 = (5, 6, [7, 8])
    # 在 outer 中修改全局变量中的可变元素
    print(f"outer 函数修改 tuple1[2][0] 之前:{tuple1}")
    tuple1[2][0] = "outer"
    print(f"outer 函数修改 tuple1[2][0] 之后:{tuple1}")
    print("=" * 20)

    def inner():
        # 在 inner 中修改全局变量中的可变元素
        print(f"inner 函数修改 tuple1[2][0] 之前:{tuple1}")
        tuple1[2][0] = "inner"
        print(f"inner 函数修改 tuple1[2][0] 之后:{tuple1}")
        print("=" * 20)

        # 在 inner 中修改自由变量中的可变元素
        print(f"inner 函数修改 tuple2[2][0] 之前:{tuple2}")
        tuple2[2][0] = "inner"
        print(f"inner 函数修改 tuple2[2][0] 之后:{tuple2}")

    inner()


outer()

images/7.函数用法/Pasted-image-20251002225116.png

Python2 中实现闭包的方式:

# Python2 中闭包的实现
def counter():
    c = [0]

    def inc():
        c[0] += 1
        return c[0]

    # 有意为之,就是要返回函数,
    # 函数用 foo 变量记住,c 闭包必须保留
    return inc

这是 Python2 中实现闭包的方式,Python3 还可以使用 nonlocal 关键字。

19.1 nonlocal 语句

nonloca:将变量标记为不在本地作用域定义,而是在上级的某一级局部作用域中定义,但不能是全局作用域中定义。即声明该变量为自由变量

Python3 中实现闭包的方式:

# Python3 中比较简单实现闭包的方式
def counter():
    count = 0

    def inc():  # inc 指向的函数对象内存地址 address1

        # count = count + 1  # 如果在一个函数中使用 c =,c 就是 inc 函数局部变量

        # non-local 不是我本地的,是我当前函数外层函数中的某一层上的 c 变量
        # 但是绝不能是全局的
        nonlocal count
        count += 1
        return count

    return inc


foo = counter()
print(foo(), foo())

count 是外层函数的局部变量,被内部函数引用。

内部函数使用 nonlocal 关键字声明 count 变量在上级作用域而非本地作用域中定义。

代码中内层函数引用外部局部作用域中的自由变量,形成闭包。

注意
> ```python > a = 50 > > > def counter(): > nonlocal a > a += 1 > > print(a) > > count = 0 > > def inc(): > nonlocal count > count += 1 > return count > > return inc > > > foo = counter() > foo() > foo() > ``` > > 上例是错误的,`nonlocal` 声明变量 `a` 不在当前作用域,但是往外就是全局作用域了,所以错误。 > > ![images/7.函数用法/Pasted-image-20251003002401.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010693-1493930661.png)

19.2 闭包内存分析

def outer():
    a = 1

    def inner():
        nonlocal a
        print(f"a:{a}")
        a += 1

    return inner


inn = outer()
inn()
inn()
inn()
inn()

执行完 inn = outer() 的内存图。outer() 栈帧执行完后实际已经消失了,画上去,是为了展现关系。

images/7.函数用法/Pasted-image-20250329201135.png

执行完 inn = outer() 的内存图。由于 inner() 内部函数的调用,outer() 栈帧消失后,局部变量 a 指向的对象 1 仍然存在。从而形成了闭包

images/7.函数用法/Pasted-image-20250329201322.png

第一次调用 inn 从而调用内部函数,仍然可以拿到以前局部变量指向的对象 1,打印 a 的值为 1,然后进行加一操作将 a 的值变为 2

images/7.函数用法/Pasted-image-20250329201805.png

第二次调用 inn,由于第一次调用 inn 时将 a 的值更新为 2,故此次调用打印 a 的值为 2,然后再进行加一操作将 a 的值变为 3

images/7.函数用法/Pasted-image-20250329201837.png

总结
闭包可以当成两个部分组成的整体:
> 1. 函数 > 2. 自由变量

闭包的作用:

  1. 隐藏变量,避免全局污染。
  2. 可以读取函数内部的变量。

同时闭包使用不当,优点就变成了缺点:

  1. 导致变量不会被垃圾回收机制回收,造成内存消耗。
  2. 不恰当的使用闭包可能会造成内存泄漏的问题。

19.3 闭包和自由变量——全局变量污染问题的解决

示例:使用全局变量实现变量自增,但污染了其他程序。

# 需求:实现变量 a 自增
# 通过全局变量,可以实现,但会污染其他程序  
a = 10


def add():
    global a
    a += 1
    print("a:", a)


def print_ten():
    if a == 10:
        print("ten!")
    else:
        print("全局变量 a,不等于 10")


add()
add()
add()
print_ten()

images/7.函数用法/Pasted-image-20251003004310.png

示例:定义局部变量,不污染,但无法递增。

# 需求:实现变量 a 自增  
# 通过局部变量,不能实现递增  
a = 10


def add():
    a = 10
    a += 1
    print("a:", a)


def print_ten():
    if a == 10:
        print("ten!")
    else:
        print("全局变量 a,不等于 10")


add()
add()
add()
print_ten()

images/7.函数用法/Pasted-image-20250329203513.png

示例:通过自由变量,可以实现递增,也不会污染其他程序。

# 需求:实现变量 a 自增
# 通过自由变量,可以实现递增,也不会污染其他程序
a = 10


def add():
    a = 10

    def increment():
        nonlocal a
        a += 1
        print("a:", a)

    return increment


def print_ten():
    if a == 10:
        print("ten!")
    else:
        print("全局变量 a,不等于 10")


increment = add()
increment()
increment()
increment()
increment()
print_ten()

images/7.函数用法/Pasted-image-20250329203919.png

19.4 闭包实现不修改源码实现添加功能——装饰器的基础(重要)

案例:用闭包实现不修改源码添加功能。

假设现在有一个已经开发完功能的函数如下:

def func():
    print("使用功能1")

现在有了一个新的需求,在使用该功能前进行日志记录。最容易想到的方法就是直接在函数中进行添加功能:

def func():
    print("日志纪录...")
    print("使用功能1")

但是这么做的话会破坏函数功能的封装性,并且每次添加新功能时都要修改已经开发完成的代码,这么做并不好。能不能在不修改已经开发完成的函数的前提下进行功能的添加呢?

可以使用闭包来实现上述的需求。

def wrapper(func):
    def inner():
        print("日志纪录...")
        func()

    return inner


def func():
    print("使用功能1")


inn = wrapper(func)
# 装饰器(闭包)
inn()

images/7.函数用法/Pasted-image-20251003005119.png

如果功能函数需要传参该如何实现?

使用 *args**kwargs 来接受参数。

def wrapper(func):
    def inner(*args, **kwargs):
        print("日志纪录...")
        func(*args, **kwargs)

    return inner


def func():
    print("使用功能1")


def func_args(a, b, c):
    print("使用功能2")
    print(a, b, c)


func1 = wrapper(func)
# 装饰器(闭包)
func1()
print("=" * 40)

func2 = wrapper(func_args)
# 装饰器(闭包)
func2(1, 2, 3)

images/7.函数用法/Pasted-image-20251003005520.png

20 默认值的作用域

看下面两例:

def foo(xyz=1):
    print(xyz)


foo()  # 打印什么?
foo()  # 打印什么?

images/7.函数用法/Pasted-image-20251003005618.png

def foo(xyz=[]):
    xyz.append(1)
    print(xyz)


foo()  # 打印什么?
foo()  # 打印什么?

images/7.函数用法/Pasted-image-20251003005743.png

为什么第二次调用 foo 函数打印的是 [1, 1]

  • xyz 是局部变量,不可以在函数外访问,这个默认值不可能保存在这个局部变量上。
  • 因为函数也是对象,每个函数定义被执行后,就生成了一个函数对象,和函数名这个标识符关联。
  • 函数是对象,有属性。Python 把函数的默认值放在了函数对象的属性中,这个属性就伴随着这个函数对象的整个生命周期。
  • 查看 foo.__defaults__ 属性,它是个元组。
def foo(xyz=[], m=123, n='abc'):
    xyz.append(1)
    m += 20
    n += '+'
    print(xyz, m, n)
    print(f"id(xyz):{id(xyz)}\nid(m):{id(m)}\nid(n):{id(n)}")


print(f"id(foo):{id(foo)}", foo.__defaults__)
print("=" * 40)
foo()
print("=" * 40)
print(f"id(foo):{id(foo)}", foo.__defaults__)
print("=" * 40)
foo()
print("=" * 40)
print(f"id(foo):{id(foo)}", foo.__defaults__)

images/7.函数用法/image-20221007201731552.png

以上结果为 Jupyter 中,是交互模式,[-5, 256] 之间的整数和会触发驻留机制,所以在 foo 函数的两次执行中,m 变量的 id 地址一样。而 n 变量是 abc+ ,不符合交互模式下字符串的驻留条件,所以两次执行的 id 不一样。

交互模式下字符串的驻留条件
1. 空字符串
> 2. 单个字符的字符串 > 3. 符合标识符格式的字符串

以下为 PyCharm 中执行的结果,在 PyCharm 不符合标识符规则的字符串也会触发驻留机制,所以变量 n 两次执行的 id 一样。

images/7.函数用法/Pasted-image-20251003010918.png

函数地址并没有变,就是说 foo 这个函数对象的没有变过,调用它,它的属性 __defaults__ 中使用元组保存默认值。

xyz 默认值是引用类型,引用类型的元素变动,并不引起元组的变化。

mn 都是非引用类型,它们保存在缺省值属性元组中,将不能再改变了。

属性 __defaults__ 中使用元组保存所有位置参数默认值,它不会因为在函数体内改变了局部变量(形参)的值而发生改变。

# keyword-only 参数的缺省值
def foo(xyz, m=123, *, n='abc', t=[1, 2]):
    m = 456
    n = 'def'
    t.append(300)
    print(xyz, m, n, t)


print(foo.__defaults__, foo.__kwdefaults__)
foo('小龙女')
print(foo.__defaults__, foo.__kwdefaults__)

images/7.函数用法/Pasted-image-20251003011231.png

属性 __defaults__ 中使用元组保存所有位置参数默认值。

属性 __kwdefaults__ 中使用字典保存所有 keyword-only 参数的默认值。

21 函数的销毁

定义一个函数就是生成一个函数对象,函数名指向的就是函数对象。

可以使用 del 语句删除函数,使其引用计数减 1。

可以使用同名标识符覆盖原有定义,本质上也是使其引用计数减 1。

Python 程序结束时,所有对象销毁。

函数也是对象,也不例外,是否销毁,还是看引用计数是否减为 0。

22 高阶函数

22.1 一等公民

images/7.函数用法/Pasted-image-20250329191245.png

  • 函数在 Python 是一等公民(First-Class Object)。
  • 函数也是对象,是可调用对象。
  • 函数可以作为普通变量,也可以作为函数的参数、返回值。

22.2 高阶函数

高阶函数(High-order Function):

  • 数学概念 y = f(g(x))
  • 在数学和计算机科学中,高阶函数应当是至少满足下面一个条件的函数
    • 接受一个或多个函数作为参数
    • 返回一个函数
  • Python 内建的高阶函数有 mapreducefiltersorted

观察下面的函数定义,回答问题:

def counter(base):
    def inc(step=1):
        base += step
        return base

    return inc
`counter` 是不是高阶函数?

`counter` 是高阶函数,因为返回值是一个函数 `inc`。
上面代码有没有问题?如果有,如何改进?

有,`inc` 函数中没有声明 `base` 变量为外层函数的变量,所以 `base` 是一个局部变量,而作为局部变量的话,`base` 还未定义就使用了。
> > 用 `nonlocal` 关键字声明 `base` 为外层函数的变量。
如何调用以完成计数功能?

> ```python > # 保证调用的是同一个 inc 函数对象 > # 这时 inc 闭包中保存的 base 对象是同一个 > # 可以实现累加 > foo = counter(0) > > print(foo()) > print(foo()) > print(foo()) > ``` > > ![images/7.函数用法/Pasted-image-20251003124801.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010623-235269284.png)
`f1 = counter(5)` 和 `f2 = counter(5)`,请问 `f1` 和 `f2` 相等吗?

不相等。
> > 首先因为 `f1` 和 `f2` 变量保存的是函数对象,而函数对象没有实现 `__eq__()` 方法,所以本来比较两个对象的值是否相等的 `f1 == f2` 就会默认转为比较两个对象的地址是否相同,即 `id(f1) == id(f2)`,等价于 `f1 is f2`,显然 `f1` 和 `f2` 是两个不同的对象。 > > ```python > def counter(base): > def inc(step=1): > nonlocal base > base += step > return base > > return inc > > > c1 = counter(5) > c2 = counter(5) > > print(f"id(c1):{id(c1)}") > print(f"id(c2):{id(c2)}") > print(f"c1 == c2:{c1 == c2}") > ``` > > ![images/7.函数用法/Pasted-image-20251003125414.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020011113-730021064.png) > > `inc` 函数对象是 `counter` 函数每一次执行时才临时创建的。

22.3 sorted 函数原理

练习:自定义 sort 函数

仿照内建函数 sorted,请自行实现一个 sort 函数(不用使用内建函数),能够为列表元素排序。

思考:通过练习,思考 sorted 函数的实现原理,mapfilter 函数的实现原理。

思路
  • 内建函数 sorted 函数,它返回一个新的列表,使用 reverse 可以设置升序或降序,使用 key 可以设置一个用于比较的函数(自定义函数也要实现这些功能)。
  • 新建一个列表,遍历原列表,和新列表中的当前值依次比较,决定待插入数插入到新列表的什么位置。

具体实现步骤:

请问下面的函数是什么排序?代码还能怎么改变?

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for x in iterable:
        for i, y in enumerate(new_list):
            if x > y:  # × > y 立即插入,说明 y 小被挤向右边。换成 × < y 是什么意思?
                new_list.insert(i, x)
                break
            else:  # 不大于。说明是最小的,尾部追加
                new_list.append(x)
    return new_list
  • x > yx < y 能控制什么?
  • 这个算法是如何实现排序的?

进一步实现 reverse 参数:

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for x in iterable:
        for i, y in enumerate(new_list):
            comp = x > y if reverse else x < y  # 实现 reverse 参数
            if comp:  # x > y 立即插入,说明 y 小被挤向右边。换成 x < y 是什么意思?
                new_list.insert(i, x)
                break
        else:  # 不大于,说明是最小的,尾部追加
            new_list.append(x)
    return new_list

进一步实现 key 参数功能,注意,key 这个函数只影响比较:

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for x in iterable:
        cx = key(x) if key else x
        for i, y in enumerate(new_list):
            cy = key(y) if key else y
            comp = cx > cy if reverse else cx < cy  # 实现 reverse 参数
            if comp:  # x > y 立即插入,说明 y 小被挤向右边。换成 x < y是什么意思?
                new_list.insert(i, x)
                break
        else:  # 不大于,说明是最小的,尾部追加
            new_list.append(x)
    return new_list

完整代码:

# v1.0
def sort(l, /, *, reverse=False, key=None):
    # 生成一个新的列表
    new_list = [l[0]]
    if not reverse:
        for i in l[1:]:
            for index, j in enumerate(new_list):
                if key:
                    if key(i) >= key(j):
                        continue
                else:
                    if i >= j:
                        continue
                new_list.insert(index, i)
                break
            else:
                new_list.append(i)
    else:
        # 使用索引进行比较,也可以继续使用上面的方法
        for i in range(1, len(l)):
            for j in range(len(new_list)):
                if key:
                    if key(l[i]) <= key(new_list[j]):
                        continue
                    else:
                        if l[i] <= new_list[j]:
                            continue
                new_list.insert(j, l[i])
                break
            else:
                new_list.append(l[i])
    return new_list


# v1.1
def sort(l, /, *, reverse=False, key=None):
    # 生成一个新的列表
    new_list = []

    for i in l:
        for index, j in enumerate(new_list):
            if key:
                flag = key(i) > key(j) if reverse else key(i) < key(j)
            else:
                flag = i > j if reverse else i < j
            if flag:
                new_list.insert(index, i)
                break
        else:
            new_list.append(i)
    return new_list


# v1.2
def sort(l, /, *, reverse=False, key=None):
    new_list = [l[0]]

    for i in l[1:]:
        temp_i = key(i) if key else i

        for index, j in enumerate(new_list):
            temp_j = key(j) if key else j
            flag = temp_i < temp_j if not reverse else temp_i > temp_j

            if flag:
                new_list.insert(index, i)
                break
        else:
            new_list.append(i)

    return new_list


if __name__ == "__main__":
    l = [1, "f", 2, 7, "a", 4, 1, 8, 10]
    print(sort(l, reverse=True,
               key=lambda x: x if isinstance(x, int) else int(x, 16)
               ))

images/7.函数用法/Pasted-image-20251003131908.png

22.4 内建高阶函数

映射 map

定义:

map(function, *iterable) -> map object

map() 函数接收两组参数,一是函数,一种是序列(可以传入多个序列),map 将传入的函数依次作用到序列的每个元素,并把结果作为新的序列返回。

  • 对多个可迭代对象的元素,按照指定的函数进行映射。
  • 返回一个迭代器。

比如我们有一个函数 $f(x)=x^2$ ,要把这个函数作用在一个 list = [1,2,3,4,5,6,7,8,9] 上,就可以用 map() 实现如下:

images/7.函数用法/Pasted-image-20250329213904.png

当然,不需要 map() 函数,也可以计算出结果,写一个循环,实现代码如下:

def f(x):
    return x * x


l = [1, 2, 3, 4, 5, 6, 7, 8, 9]
L = []

for n in l:
    L.append(f(n))

print(L)

images/7.函数用法/Pasted-image-20250329214244.png

示例:map 高阶函数的使用案例。

def f(x):
    return x * x


l = [1, 2, 3, 4, 5, 6, 7, 8, 9]

L = map(f, l)
print(L)
print(list(L))
print(list(L))

images/7.函数用法/Pasted-image-20251003154922.png

示例:map 高阶函数的使用案例(用匿名函数)。

l = [1, 2, 3, 4, 5, 6, 7, 8, 9]

L = map(lambda x: x * x, l)

print(list(L))

images/7.函数用法/Pasted-image-20250329214716.png

示例:map 函数传入两个列表。

def f2(x, y):
    return x + y


L = map(f2, [1, 2, 3, 4], [10, 20, 30])

print(list(L))

images/7.函数用法/Pasted-image-20250329215126.png

示例:map 函数传入两个列表(用匿名函数)。

L = map(lambda x, y: x + y, [1, 2, 3, 4], [10, 20, 30])

print(list(L))

images/7.函数用法/Pasted-image-20250329215223.png

示例:map 函数的用法。

res1 = list(map(lambda x: 2 * x + 1, range(10)))
res2 = dict(map(lambda x: (x % 5, x), range(500)))
res3 = dict(map(lambda x, y: (x, y), 'abcde', range(10)))

print(res1)
print(res2)
print(res3)

images/7.函数用法/Pasted-image-20251003155146.png

reduce

定义:

def reduce(function: (_T, _S) -> _T,
           sequence: Iterable[_S],
           initial: _T,
           /) -> _T

images/7.函数用法/Pasted-image-20250329215502.png

reduce 位于 functools 模块。

reduce 把一个函数作用在一个序列 [x1, x2, x3…] 上,这个函数必须接收两个参数reduce 把结果继续和序列的下一个元素做累积计算,其效果就是:

reduce(f, [x1, x2, x3, x4]) <==> f(f(f(x1, x2), x3), x4)

示例:reduce 实现对一个序列求和。

from functools import reduce


def add(x, y):
    return x + y


sum = reduce(add, [1, 3, 5, 7, 9])
print(sum)

images/7.函数用法/Pasted-image-20250329220034.png

过滤 filter

定义:

filter(function, iterable)

images/7.函数用法/Pasted-image-20250329220546.png

内置函数 filter() 用于过滤序列。filter() 把传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃该元素。

  • 对可迭代对象进行遍历,返回一个迭代器。
  • function 参数是一个带参数的函数,且返回值应当是 bool 类型,或其返回值等效布尔值。
  • function 参数如果是 None,则使用可迭代对象的每一个元素自身等效布尔值来判断是否丢弃该元素。

示例:filter 过滤列表,删掉偶数,只保留奇数。

# 在一个 list 中,删掉偶数,只保留奇数
def is_odd(n):
    return n % 2 == 1


L = filter(is_odd, [1, 2, 4, 5])

print(L)
print(list(L))
print(list(L))

images/7.函数用法/Pasted-image-20250329221008.png

示例:filter 过滤列表,删掉偶数,只保留奇数,使用匿名函数实现。

L = filter(lambda x: x % 2 == 1, [1, 2, 4, 5])

print(list(L))

images/7.函数用法/Pasted-image-20250329221347.png

示例:filter 序列中的空字符串删掉。

def not_empty(s):
    return s and s.strip()


L = filter(not_empty, ['A', '', 'B', None, 'c', ' '])

print(list(L))

images/7.函数用法/Pasted-image-20250329221716.png

示例:filter 序列中的空字符串删掉,使用匿名函数实现。

L = filter(lambda x: x and x.strip(), ['A', '', 'B', None, 'c', ' '])

print(list(L))

images/7.函数用法/Pasted-image-20250329221857.png

示例:filter 函数的用法。

res1 = list(filter(lambda x: x % 3 == 0, [1, 9, 55, 150, -3, 78, 28, 123]))
res2 = list(filter(None, range(5)))
res3 = list(filter(None, range(-5, 5)))

print(res1)
print(res2)
print(res3)

images/7.函数用法/Pasted-image-20251003160448.png

排序 sorted

定义:

sorted(iterable, *, key=None, reverse=False) -> list

排序算法,排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。

如果是数字,我们可以直接比较。

思考
如果是**自定义对象**呢?

直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。
> > 使用 `functools.cmp_to_key` 来指定比较的函数。例如 `key=cmp_to_key(custom_sorted)`。 > > 通常规定在比较函数中,**对于两个元素 `x` 和 `y`,如果认为 `x < y`,则返回 `-1`,如果认为 `× == y`,则返回 `0`,如果认为 `x > y`,则返回 `1`**。这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。

示例:sortedlist 进行排序。

sorter = sorted([1, 3, 6, -20, 34])

print("升序排列:", sorter1)

images/7.函数用法/Pasted-image-20250329222532.png

sorted() 函数也是一个高阶函数,它还可以接收一个 key 函数来实现自定义的排序。

示例:sorted 函数接收一个 key 自定义排序。

sorter1 = sorted([1, 3, 6, -20, 34])
print("升序排列:", sorter1)

# sorted() 函数也是高阶函数,它还可以接收一个 key 函数来实现自定义的排序
sorter2 = sorted([1, 3, 6, -20, -70], key=abs)
print("自定义排序:", sorter2)

sorter2 = sorted([1, 3, 6, -20, -70], key=abs, reverse=True)
print("自定义反向排序:", sorter2)

# 4.2 字符串排序依照 ASCII
sorter3 = sorted(["ABc", "abc", "D", "d"])
print("字符串排序:", sorter3)

# 4.3 忽略大小写排序
sorter4 = sorted(["ABc", "abc", "D", "d"], key=str.lower)
print("忽略字符串大小写排序:", sorter4)

# 4.4 要进行反向排序,不必改动 key 函数,可以传入第三个参数 reverse = True
sorter5 = sorted(["ABc", "abc", "D", "d"], key=str.lower, reverse=True)
print("忽略字符串大小写反向排序:", sorter5)

images/7.函数用法/Pasted-image-20250329223243.png

示例:sorted 对自定义对象的排序。

from functools import cmp_to_key


class Student:
    def __init__(self, age, name):
        self.name = name
        self.age = age


def custom_sorted(stu1, stu2):
    if stu1.age < stu2.age:
        return -1
    if stu1.age > stu2.age:
        return 1
    return 0


stu1 = Student(41, 'aaa')
stu2 = Student(21, 'ccc')
stu3 = Student(31, 'bbb')

# 使用匿名函数进行排序比较
# student_list = sorted([stu1, stu2, stu3], key=lambda x: x.age)
# 使用自定义函数进行排序比较,在比较复杂的对象判断中判断条件无法一行完成,
# 此时就需要自定义的比较函数了。
student_list = sorted([stu1, stu2, stu3], key=cmp_to_key(custom_sorted))

for stu in student_list:
    print('name:', stu.name, 'age:', stu.age)

images/7.函数用法/Pasted-image-20250329224259.png

示例:sorted 函数的用法。

lst = [1, 2, 3, 4, 5]
sorted(lst, key=lambda x: 6 - x)  # 返回新列表
lst.sort(key=lambda x: 6 - x)  # 就地修改
  • sorted 函数处理过一个列表后,排序生成的新列表和原列表元素个数一致吗?
    一致的。
  • filter 处理一个列表后,请问迭代出的元素个数和原列表一致吗?
    过滤,不一定一致。
  • map 把一个列表的所有元素,一个都不能少,从一种形式转换到另一种形式。

23 生成器 ★★★

images/7.函数用法/Pasted-image-20250330215911.png

生成器定义

在 Python 中,一边循环一边计算的机制,称为生成器 generator

生成器 generator

  • 生成器指的是生成器对象
    1. 可以由生成器表达式得到。
    2. 也可以使用 yield 关键字编写一个生成器函数,调用这个函数得到一个生成器对象。
  • 生成器对象,是一个可迭代对象,是一个迭代器
  • 生成器对象,是延迟计算、惰性求值的
思考
> 为什么要有生成器?
列表所有数据都在内存中,如果有海量数据的话将会非常耗内存。
> > 如:仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。 > > 如果列表元素可以按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的 `list`,从而节省大量的空间。
总结
- 时间换空间!想要得到庞大的数据,又想让它占用空间少,那就用生成器!延迟计算!
> - 需要的时候,再计算出数据!

23.1 创建生成器的方式一(生成器表达式)

生成器表达式很简单,只要把一个列表推导式的 [] 改成 (),就创建了一个生成器 generator

L = [x * x for x in range(10)]
print(L)

g = (x * x for x in range(10))
print(g)  # <generator object <genexpr> at 0x1022ef630>

images/7.函数用法/Pasted-image-20250330221819.png

创建 Lg 的区别仅在于最外层的 []()L 是一个 list,而 g 是一个 generator

23.2 创建生成器的方式二(生成器函数)

  • 如果一个函数中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个生成器函数调用函数就是创建了一个生成器对象 generator
  • 生成器函数:其实就是利用关键字 yield 一次返回一个结果,然后阻塞,然后再重新开始。
m = (i for i in range(5))  # 生成器表达式

print(type(m))
print(next(m))
print(next(m))


def inc():  # 生成器函数
    for i in range(5):
        yield i


print(type(inc))
print(type(inc()))

g = inc()
print(type(g))
print(next(g))

for i in g:
    print(i)

images/7.函数用法/Pasted-image-20251003172748.png

生成器只能使用一次,第一次 for 循环遍历完后就没有元素了。如果再用 next() 调用空生成器,会报 StopIteration 异常

def inc():
    for i in range(3):
        yield i


g = inc()
print(next(g))
print(next(g))
print(next(g))
print(next(g))

images/7.函数用法/Pasted-image-20251003172850.png

注意
- 普通函数调用,函数会立即执行直到执行完毕。
> > - 生成器函数调用,并不会立即执行函数体,而是返回一个生成器对象,需要使用 `next` 函数来驱动这个生成器对象,或者使用循环来驱动。 > > - 生成器表达式和生成器函数都可以得到生成器对象,只不过**生成器函数可以写更加复杂的逻辑**。

看下面的例子:

def foo():
    print(1)
    yield 2
    print(3)
    yield 4
    print(5)
    return 6
    yield 7


print(f"返回:{next(foo())}")  # 第一次返回什么
print(f"返回:{next(foo())}")  # 第二次返回什么
第 11 行和第 12 行分别打印什么内容?返回什么内容?

第 11 行和第 12 行打印的都是 1,返回的都是 2。
> > 因为 `foo()` 是在调用函数,每调用一次就返回一个新的生成器对象。即两次操作的是不同的生成器对象,所以两次 `next` 都相当于两个对象只拨动了一次。
print(f"返回:{next(foo())}")  # 第一次返回什么
print(f"返回:{next(foo())}")  # 第二次返回什么

images/7.函数用法/Pasted-image-20251003174042.png

def foo():
    print("打印 1")
    yield 2
    print("打印 3")
    yield 4
    print("打印 5")
    return 6
    yield 7


g = foo()
print(f"返回:{next(g)}")  # ? 第一次返回什么
print(f"返回:{next(g)}")  # ? 第二次返回什么
print(f"返回:{next(g)}")  # ? 第三次返回什么
print(f"返回:{next(g, 'END')}")  # next(g, 'END') 没有元素返回缺省值 'END'

这次的代码用 g 记住了 foo() 函数返回的生成器对象,即操作的是同一个生成器。

第一次返回什么?

第一次拨动一下生成器执行 `foo` 中的语句遇到第一个 `yield` 语句后暂停,打印 1,返回一个 2。
第二次返回什么?

第二次拨动一下生成器执行 `foo` 中第一个 `yield` 语句后的语句,遇到第二个 `yield` 语句后暂停,打印 3,返回一个 4。
第三次返回什么?

第三次拨动一下生成器执行 `foo` 中第二个 `yield` 语句后的语句,遇到了 `return` 语句,生成器函数已经执行完毕,相当于拨动了空的生成器,抛出 `StopIteration` 异常,打印 5。

images/7.函数用法/Pasted-image-20251003175206.png

最后一句会执行吗?如果执行返回什么?如果不能执行怎么调整?

最后一句因为 `foo` 函数在执行到 `return` 时便已经结束,会抛出 `StopIteratrion` 错误,程序会异常退出,无法执行。
> > 可以通过异常处理接收 `StopIteratrion` 错误,保证主程序正常执行。 > > 在进行异常处理后,`foo` 函数之前已经执行到了 `return` 语句,所以 `yield 7` 是无效语句,即生成器已经为空,而由于调用 `next()` 函数时指定了生成器为空时返回的默认值,所以返回 `END`。
g = foo()

print(f"返回:{next(g)}")  # ? 第一次返回什么
print(f"返回:{next(g)}")  # ? 第二次返回什么

try:
    print(f"返回:{next(g)}")  # ? 第三次返回什么
except StopIteration:
    print("生成器中的元素已经全部取出!!!")

print(f"返回:{next(g, 'END')}")  # next(g, 'END') 没有元素返回缺省值 'END'

images/7.函数用法/Pasted-image-20251003180810.png

总结
在生成器函数中,可以写多个 `yield` 语句,每执行一次 `yield` 语句后会暂停执行,并将 `yield` 表达式的值返回;再次执行时会执行到下一个 `yield` 语句然后再次暂停执行,并返回 `yield` 表达式的值;一直重复此操作,直到遇到 `return` 语句,代表生成器的值已经取空,抛出 `StopIteration` 异常。
> > 函数返回: > > - `return` 语句依然可以终止函数运行,但 `return` 语句的返回值不能被获取到。 > - `return` 会导致当前函数返回,无法继续执行,也无法继续获取下一个值,抛出 `StopIteration` 异常。 > - 如果函数没有显式的 `return` 语句,如果生成器函数执行到结尾(相当于执行了 `return None`,一样会抛出 `StopIteration` 异常。 > > 生成器函数: > > - 包含 `yield` 语句的生成器函数调用后,生成生成器对象的时候,**生成器函数的函数体并不会立即执行**。 > - `next(generator)` 会从函数的当前位置向后执行直到碰到第一个 `yield` 语句之后,会弹出值,并暂停函数执行。 > - 再次调用 `next` 函数,和上一条一样的处理过程。 > - 继续调用 `next` 函数,生成器函数如果结束执行了(显式或隐式调用了 `return` 语句),会抛出 `StopIteration` 异常。

23.3 生成器函数的工作原理

  • 生成器函数返回一个迭代器,for 循环对这个迭代器不断调用 __next__() 函数,不断运行到下一个 yield 语句,一次一次取得每一个返回值,直到没有 yield 语句为止,最终引发 StopIteration 异常。
  • yield 相当于 return 返回一个值,并且记住这个返回的位置,下次迭代时,代码从 yield 的下一条语句(不是下一行)开始执行。例如 temp = yield i 下次迭代时执行的是 temp 变量的赋值语句,而不是直接执行下一行语句。
  • send()next() 一样,都能让生成器继续往下走一步(下次遇到 yield 停),但 send() 能传一个值,这个值作为 yield 表达式整体的结果。yield 表达式没有返回值,即在 res = yield 1 中,res 的值为 None,如果想给变量 res 赋一个值,就需要用 send(value) 进行传递。这样最终 res = value
> 生成器推导式底层原理也是这样的。
```python """ 1. 函数有了 yield 之后,调用它,就会生成一个生成器 2. yield 作用:程序挂起,返回相应的值。下次从下一个语句开始执行。 3. return 在生成器中代表生成器终止,直接报错:StopIteration 4. next 方法作用:唤醒并继续执行 """

def test():
print("start")

i = 0
while i < 3:
    temp = yield i  # 下次迭代时,代码从 yield 的下一条语句(不是下一行)开始执行
    print(f"temp:{temp}")
    i += 1

print("end")
return "done"

if name == 'main':
a = test()
print(type(a))
print(a.next()) # next(a) 一样
print(a.send("我是 send 传进来的值"))
print(a.next())
print(a.next()) # 抛出异常:StopIteration


![images/7.函数用法/Pasted-image-20251003191141.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020011360-560250829.png)

### 23.4 生成器应用

#### 无限循环

```python
def counter():
    i = 0
    while True:
        i += 1
        yield i


c = counter()
print(next(c))  # 打印什么
print(next(c))  # 打印什么
print(next(c))  # 打印什么

images/7.函数用法/Pasted-image-20251003191852.png

计数器

def inc():
    def counter():
        i = 0
        while True:
            i += 1
            yield i

    c = counter()
    return next(c)


print(inc())  # 打印什么?
print(inc())  # 打印什么?
print(inc())  # 打印什么?为什么?怎么修改?

images/7.函数用法/Pasted-image-20251003192219.png

images/7.函数用法/image-20221015175248332.png

inc 调用返回的是一个整型值,所以三次 print 打印值都是一样的。

修改后:

def inc():
    def counter():
        i = 0
        while True:
            i += 1
            yield i

    c = counter()

    def inner():
        return next(c)

    return inner

    # inner 函数只有一行,可以直接用 lambda 表达式替换
    # return lambda : next(c)


foo = inc()
print(foo())  # 打印什么?
print(foo())  # 打印什么
print(foo())  # 打印什么?为什么?

images/7.函数用法/Pasted-image-20251003192555.png

foo 得到了 inner 函数的对象地址,而 inner 函数通过闭包记住了 inc 函数中的局部变量生成器 c,并且每调用一次 inner 函数就拨动一次生成器 c,所以生成器 c 的返回值可以不断累加。

斐波那契数列

def fib():
    # 当前次数的值
    a = 0
    # 下一次的值,第一个计算值
    b = 1

    def inc():
        while True:
            nonlocal a, b
            a, b = b, a + b
            yield a

    c = inc()
    return lambda: next(c)


g = fib()
for i in range(5):
    print(g())

images/7.函数用法/Pasted-image-20251003192851.png

生成器交互

Python 提供了一个和生成器对象交互的方法 send,该方法可以和生成器沟通。

  • yield 语句的返回值为 None
  • 调用 send 方法,就可以把 send 的实参传给 yield 语句做为返回值,这个返回值可以在等式右边被赋值给其它变量。
  • sendnext 一样可以推动生成器启动并执行。

示例:测试生成器工作原理(send)。

# send 的作用是唤醒并继续执行,发送一个信息到生成器内部
def foo():
    print("start")
    i = 0
    while i < 2:
        temp = yield i
        print(f"temp:{temp}")
        i = i + 1
    print("end")


g = foo()
print(next(g))
print("*" * 20)
print(g.send(100))
print(next(g))

images/7.函数用法/Pasted-image-20250331004140.png

示例:重置功能的计数器。

# 重置功能的计数器
def counter():
    def inc():
        i = 0
        while True:
            i += 1
            flag = yield i
            if flag:
                i = 0

    c = inc()
    return lambda flag=False: c.send(True) if flag else next(c)


# %%

foo2 = counter()
print(foo2())
print(foo2())
print(foo2())
# 重置生成器
print(foo2(True))
print(foo2())
print(foo2())

images/7.函数用法/Pasted-image-20251003193341.png

images/7.函数用法/image-20221015182316642.png

协程 Coroutine

  • 生成器的高级用法。
  • 它比进程、线程轻量级,是在用户空间调度函数的一种实现。
  • Python3 asyncio 就是协程实现,已经加入到标准库。
  • Python3.5 使用 asyncawait 关键字直接原生支持协程。
  • 协程调度器实现思路(该种实现协程的方式已经淘汰
    有 2 个生成器 AB
    • next(A) 后,A 执行到了 yield 语句暂停,然后去执行 next(B)B 执行到 yield 语句也暂停,然后再次调用 next(A),再调用 next(B) 在,周而复始,就实现了调度的效果。
    • 可以引入调度的策略来实现切换的方式。
  • 协程是一种非抢占式调度。

23.5 yield from 语法

从 Python3.3 开始增加了 yield from 语法:

yield from iterable
======等价于======
for item in iterable:
	yield item

yield from 就是一种简化语法的语法糖。

def inc():
    for x in range(1000):
        yield x


# 使用 yield from 简化
def inc1():
    # 注意这个函数出现了 yield,也是生成器函数
    yield from range(1000)


foo = inc1()
print(next(foo))
print(next(foo))
print(next(foo))

images/7.函数用法/Pasted-image-20251003193657.png

> 本质上 `yield from` 的意思就是从 `from` 后面的可迭代对象中拿元素一个个 `yield` 出去。
总结
**什么事生成器?**
> > 生成器仅仅保存了一套生成数值的算法,并且没有让这个算法现在就开始执行,而是什么时候调它,它什么时候开始计算一个新的值,并给你返回。 > > **生成器特点:** > - 生成器函数生成一系列结果。通过 `yield` 关键字返回一个值后,还能从其退出的地方继续运行,因此可以随时间产生一系列的值。 > - 生成器和迭代是密切相关的,迭代器都有一个 `__next__()` 成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。 > - 生成器是一个特殊的程序,可以被用作控制循环的迭代行为,Python 中**生成器是迭代器的一种**,使用 `yield` 返回值函数,每次调用 `yield` 会暂停,而可以使用 `next()` 函数和 `send()` 函数恢复生成器。 > - 生成器看起来像是一个函数,但是表现得却像是迭代器。

24 迭代器

images/7.函数用法/Pasted-image-20250331095024.png

概念

  • 迭代是 Python 最强大的功能之一,是访问集合元素的一种方式。
  • 迭代器是一个可以记住遍历的位置的对象。
  • 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。
  • 迭代器只能往前不会后退。
  • 迭代器有两个基本的方法:iter()next()
  • 一个实现了 iter 方法的对象,称为可迭代对象 Iterable
  • 一个实现 next 方法并且是可迭代的对象,称为迭代器 Iterator。即:实现了 iter 方法和 next 方法的对象就是迭代器。
注意
生成器都是 `Iterator` 对象。但 `list`、`dict`、`str` 虽然是 `Iterable`(可迭代对象),却不是 `Iterator`(迭代器)。
> > ```python > # Python3.6 之前不加 .abc,之后的加 > from collections.abc import Iterator > from collections.abc import Iterable > > a = isinstance([], Iterable) > b = isinstance([], Iterator) > > print(f"列表是否为可迭代对象:{a}") > print(f"列表是否为迭代器:{b}") > ``` > > ![images/7.函数用法/Pasted-image-20250331101242.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020011723-1427871723.png)
注意
`list`、`dict`、`str` 等 `Iterable` 变成 `Iterator`,可以使用 `iter()` 函数:
> > ```python > # python3.6之前不加.abc,之后的加 > from collections.abc import Iterator > > a = isinstance(iter([]), Iterator) > b = isinstance(iter('abc'), Iterator) > > print(f"iter([]) 是否为迭代器:{a}") > print(f"iter('abc') 是否为迭代器:{b}") > ``` > > ![images/7.函数用法/Pasted-image-20250331101739.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020010800-1983209266.png)

24.1 生成器、迭代器和可迭代对象

images/7.函数用法/Pasted-image-20250331103456.png

为什么 listdictstr 等数据类型不是 Iterator

Python 的 Iterator 对象表示的是一个数据流。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next() 函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。所以,生成器一定是迭代器。

Iterator 甚至可以表示一个无限大的数据流,例如全体自然数。而使用 list 是永远不可能存储全体自然数的。

24.2 for 循环的本质

Python3 的 for 循环本质上就是通过不断调用 next() 函数实现的。

for x in [1, 2, 3, 4, 5]:
	pass

本质是:

# 首先获得 Iterator 对象
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值
        x = next(it)
    except StopIteration:
        # 遇到 StopIteration 就退出循环
        break

24.3 创建一个迭代器

注意
忘记类相关知识可以先跳过,等复习完类知识再看。为了知识的连贯放在了此处。

一个类作为一个迭代器使用需要在类中实现两个方法 __iter__()__next__()

  • __iter__() 方法返回一个特殊的迭代器对象,这个迭代器对象实现了 __next__() 方法并通过 StopIteration 异常标识迭代的完成。
  • __next__() 方法会返回下一个迭代器对象。

示例:创建一个依次返回 10, 20, 30, … 这样数字的迭代器。

class MyNumbers:
    def __iter__(self):
        self.num = 10
        return self

    def __next__(self):
        if self.num < 40:
            x = self.num
            self.num += 10
            return x
        else:
            raise StopIteration


myclass = MyNumbers()
my_iter = iter(myclass)
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))

images/7.函数用法/Pasted-image-20250331114942.png

25 柯里化 ★★

  • 指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
  • z = f(x, y) 转换成 z = f(x)(y) 的形式。

[[柯里化详解]]

例如

def add(x, y):
    return x + y

原来函数调用为 add(4, 5),柯里化目标是 add(4)(5)。如何实现?

每一次括号说明是函数调用,说明 add(4)(5) 是两次函数调用。

def add(x):
    def inner(y):
        return x + y

    return inner


print(add(4)(5))
fn = add(4)  # add(4) 返回 func 高阶函数
result = fn(5)
print(result)

images/7.函数用法/Pasted-image-20251004003212.png

案例:

def add(x, y, z):
    return x + y + z  # add(4, 5, 6)

将上面的函数调用改成下面的方式:

  • add(4)(5, 6)

    def add(x):
        def inner(y, z):
            return x + y + z
    
        return inner
    
    
    print(add(4)(5, 6))
    
    

    images/7.函数用法/Pasted-image-20251004003438.png

  • add(4)(5)(6)

      def add(x):
          def outer(y):
              def inner(z):
                  return x + y + z
      
              return inner
      
          return outer
      
      
      print(add(4)(5)(6))
    

    images/7.函数用法/Pasted-image-20251004003438.png

  • add(4, 5)(6)

      def add(x, y):
          def inner(z):
              return x + y + z
      
          return inner
      
      
      print(add(4, 5)(6))
    

    images/7.函数用法/Pasted-image-20251004003438.png

26 装饰器 ★★★

images/7.函数用法/Pasted-image-20250403223454.png

概念

装饰器来自 Decorator 的直译。什么叫装饰,就是装点、提供一些额外的功能。在 Python 中的装饰器则是提供了一些额外的功能。

装饰器本质上是一个 Python 函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

装饰器用于有以下场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景

26.1 由来

需求:为一个加法函数增加记录实参的功能。

def add(x, y):
    print('add called. x={}, y={}'.format(x, y))  # 增加计数功能
    return x + y

add(4, 5)

上面的代码满足了需求,但有缺点:

记录信息的功能,可以是一个单独的功能。显然和 add 函数耦合太紧密。加法函数属于业务功能,输出信息属于非功能代码,不该放在 add 函数中。

  1. 提供一个函数 logger 完成记录功能。

    def add(x, y):
        return x + y
    
    
    def logger(fn):
        print('调用前增强')
        ret = fn(4, 5)
        print('调用后增强')
        return ret
    
    
    print(logger(add))
    

    images/7.函数用法/Pasted-image-20251004132830.png

  2. 改进传参。

    def add(x, y):
        return x + y
    
    
    def logger(fn, *args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs)  # 参数解构
        print('调用后增强')
        return ret
    
    
    print(logger(add, 4, 5))
    

    images/7.函数用法/Pasted-image-20251004132830.png

  3. 柯里化。

    def add(x, y):
        return x + y
    
    
    def logger(fn):
        def wrapper(*args, **kwargs):
            print('调用前增强')
            ret = fn(*args, **kwargs)  # 参数解构
            print('调用后增强')
            return ret
    
        return wrapper
    

    调用:

    logger(add)(4, 5)
    

    images/7.函数用法/Pasted-image-20251004133224.png

    或者:

    inner = logger(add)
    res = inner(4, 5)
    print(res)
    

    images/7.函数用法/Pasted-image-20251004132830.png

    再进一步:

    def add(x, y):
        return x + y
    
    
    def logger(fn):
        def wrapper(*args, **kwargs):
            print('调用前增强')
            ret = fn(*args, **kwargs)  # 参数解构
            print('调用后增强')
            return ret
    
        return wrapper
    
    
    add = logger(add)
    print(add(4, 5))
    

    images/7.函数用法/Pasted-image-20251004132830.png

  4. 装饰器语法。

    def logger(fn):
        def wrapper(*args, **kwargs):
            print('调用前增强')
            ret = fn(*args, **kwargs)  # 参数解构
            print('调用后增强')
            return ret
    
        return wrapper
    
    
    @logger  # 等价于 add = logger(add) <=> add = wrapper
    def add(x, y):
        return x + y
    
    
    print(add(4, 5))
    

    images/7.函数用法/Pasted-image-20251004132830.png

    @logger 就是装饰器语法。

等价式非常重要,如果不熟悉装饰器,开始时一定要把等价式写在后面

26.2 装饰器解决日志问题(分三个版本)

v1.0 版本解决 ==> 直接在功能函数中添加日志记录功能。

def fun1():
    print("使用功能1")
    print("日志记录")


def fun2():
    print("使用功能2")
    print("日志记录")

日志功能许多函数都要使用,而每个函数的日志的逻辑都差不多,所以我们想到可以将日志功能封装成函数来进行调用。

v2.0 版本解决 ==> 函数调用方式

def writeLog():
    print("日志纪录")


def fun1():
    print("使用功能1")
    writeLog()


def fun2():
    print("使用功能2")
    writeLog()

将日志功能封装成函数然后在函数中调用的方式相对来说耦合度还是有些高,且要在功能函数中增加调用代码,使得函数的独立性不太好。能不能在不修改函数的情况下新增日志功能呢?可以,使用闭包。

v3.0 版本解决 ==> 使用闭包

def outfunc(func):
    def infunc():
        func()
        print("日志纪录")

    return infunc


def fun1():
    print("使用功能1")


def fun2():
    print("使用功能2")


fun1 = outfunc(fun1)  # 装饰器(闭包)
fun1()

以上两种方式都不符合开闭原则,而闭包可以解决。但是在调用函数时首先需要调用外层函数返回功能函数,然后才能实现功能的调用。有没有办法简化调用步骤呢?可以使用装饰器。

> 开闭原则:对增加新功能开放;对修改已有代码关闭。
v4.0 版本解决,装饰器
def outfunc(func):
    def infunc():
        func()
        print("日志纪录")

    return infunc


@outfunc
def fun1():
    print("使用功能1")


@outfunc
def fun2():
    print("使用功能2")


fun1()
fun2()

修改变量名,见名知意。

def mylog(func):
    def infunc():
        func()
        print("日志纪录")

    return infunc


@mylog
def fun1():
    print("使用功能1")


@mylog
def fun2():
    print("使用功能2")


fun1()
fun2()

增加参数处理,可以装饰任意多个参数的函数。

def mylog(func):
    def infunc(*args, **kwargs):
        func(*args, **kwargs)
        print("日志纪录")

    return infunc


@mylog
def fun1():
    print("使用功能1")


@mylog
def fun2(a, b):
    print(f"使用功能2:{a},{b}")


fun1()
fun2(100, 200)

装饰器本质上是一个 Python 函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

装饰器用于有以下场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景

26.3 无参装饰器

  • @ 标识符。
  • 标识符指向一个函数,用一个函数来装饰它下面的函数,logger 函数称为装饰器函数或包装函数add 称为被装饰或被包装函数
  • logger 习惯上称为 wrapper
  • add 习惯上 wrapped
    • 本质上来看无参装饰器 logger 实际上是等效为一个参数的函数

@logger 会把它下面紧挨着的函数的标识符提上来作为它的实参 add = logger(add)

# 无参装饰器 logger
# @logger 会把它下面紧挨着的函数的标识符提上来作为它的实参 add = logger(add)
@logger
def add():
    pass
# 装饰器,用来装饰的,一般来讲装饰函数或类
def logger(fn):  # 2. fn <== add 指向的函数对象。logger => addr2
    def wrapper(*args, **kwargs):
        # 3. wrapper ==> addr3,把 fn 对应的函数对象困在 wrapper 函数的属性上
        # 4. addr3 对象的属性上记录着闭包,fn 指向了 addr1
        print('调用前增强功能')
        print(f'{fn.__name__} function called args {args} and kwargs {kwargs}')  # add.__name__
        ret = fn(*args, **kwargs)  # fn 是原来的函数,为什么?引用计数,闭包。addr1 的函数 (1, 200)
        print('调用后增强功能')
        return ret

    return wrapper


@logger  # add = logger(add) # @logger 称为无参装饰器 add = wrapper
def add(x, y):  # 1. 业务功能加法,增加一个功能:能不能记录一下调用参数。 add ==> addr1
    return x + y


print(add(1, 2))

images/7.函数用法/Pasted-image-20251004151326.png

  • 上例的装饰器语法,称为无参装饰器。

  • @ 符号后是一个函数。

  • 虽然叫无参装饰器,但是 @ 后的函数本质上是单参函数(或等效为单参函数)

    def logger(fn):
        pass
    ------------------------------
    # 默认参数可以不写,调用时等效于一个单参函数
    def logger(fn, x=100):
        pass
    
    @logger  # 等效于 add = logger(add)
    def add(x, y):
        pass
    ------------------------------
    如果想要给 x 赋值,就必须将 logger 柯里化
    ==============错误写法==============
    @logger(x=10)  # 等效于 add = logger(x=10)(add)
    def add(x, y):
        pass
    
  • 上例的 logger 函数是一个高阶函数。

日志记录装饰器实现

import time
import datetime


def logger(fn):
    def wrapper(*args, **kwargs):
        strat = datetime.datetime.now()
        res = fn(*args, **kwargs)  # 参数解构
        delta = (datetime.datetime.now() - strat).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    return wrapper


@logger  # 等价于 add = logger(add) <=> add = wrapper
def add(x, y):
    time.sleep(2)
    return x + y


print(add(4, 5))

images/7.函数用法/Pasted-image-20251004152553.png

26.4 装饰器本质

image-20221016154332305

如何类比上图和装饰器呢?

  • 习惯上 add 函数称为被包装函数 wrapped,增强它的函数称为包装器、包装函数 wrapper
  • 被包装函数就是上图中的画,包装的目的是增强,而不是破坏画,采用非侵入式代码。
  • 可以在原画前或后加入增强代码。
  • 装饰器如同画框,装饰器可以更换,如同更换画框,画不变。也可以画框不变,更换画。也可以画框外再加其它装饰。

26.5 多个装饰器

有时候,我们需要多个装饰器修饰一个函数。比如:需要增加日志功能、增加执行效率测试功能。

装饰器函数的执行顺序是分为(被装饰函数)定义阶段和(被装饰函数)执行阶段的,装饰器函数在被装饰函数定义好后立即执行

  • 函数定义阶段:执行顺序是从最靠近函数的装饰器开始,自内而外的执行
  • 函数执行阶段:执行顺序由外而内,一层层执行。

示例:多个装饰器执行顺序。

@mylog
@cost_time
# 函数定义阶段:
# 相当于:
# fun2 = cost_time(fun2)
# fun2 = mylog(fun2)
# 也相当于:
# fun2 = mylog(cost_time(fun2))
# 定义阶段:先执行 cost_time 函数,再执行 mylog 函数
def fun2():
    pass


# 调用执行阶段
# 先执行 mylog 的内部函数,再执行 cost_time 的内部函数
fun2()

示例:增加日志和执行计时功能的装饰器。

import time


def mylog(func):
    print("mylog start")

    def infunc():
        print("日志纪录 start")
        func()
        print("日志纪录 end")

    print("mylog end")
    return infunc


def cost_time(func):
    print("cost time start")

    def infunc():
        print("开始计时..")
        start = time.time()
        func()
        end = time.time()
        print(f"耗费时间:{end - start}")
        return end - start

    print("cost time end")
    return infunc


@mylog
@cost_time
# 相当于:
# fun2 = cost_time(fun2)
# fun2 = mylog(fun2)
# 也相当于:
# fun2 = mylog(cost_time(fun2))
def fun2():
    print("使用功能2")
    time.sleep(2)
    print("使用功能22")


print("开始使用功能")
fun2()

images/7.函数用法/Pasted-image-20250403235209.png

注意
装饰器函数在被装饰的函数定义好后**立即执行**。

26.6 带参装饰器

想要实现带参装饰器,只需要在原来的无参装饰器外再包装一层函数即可

示例:带参数的装饰器。

# 带参数的装饰器的典型写法
def mylog(type, *wrapper_args, **wrapper_kwargs):
    def decorator(func):
        def infunc(*args, **kwargs):
            if type == "文件":
                print("文件中:日志纪录")
            else:
                print("控制台:日志纪录")

            print(f"wrapper_args:{wrapper_args}, wrapper_kwargs:{wrapper_kwargs}")

            return func(*args, **kwargs)

        return infunc

    return decorator


# @mylog <==> mylog("文件")(fun2)
@mylog("文件", 1, 2, name="小龙女")
def fun2(a, b):
    print("使用功能2", a, b)


if __name__ == '__main__':
    fun2(100, 200)

images/7.函数用法/Pasted-image-20251004160401.png

示例:只在函数调用前做一些增强功能。

# 只在函数调用前做一些增强功能
def mylog(type, *wrapper_args, **wrapper_kwargs):
    def decorator(func):
        if type == "文件":
            print("文件中:日志纪录")
        else:
            print("控制台:日志纪录")

        print(f"wrapper_args:{wrapper_args}, wrapper_kwargs:{wrapper_kwargs}")

        return func

    return decorator

images/7.函数用法/Pasted-image-20251004160406.png

文档字符串

  • Python 文档字符串 Documentation Strings
  • 在函数(类、模块)语句块的第一行,且习惯是多行的文本,所以多使用三引号。
  • 文档字符串也算是合法的一条语句。
  • 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述。
  • 可以使用特殊属性 __doc__ 访问这个文档。
def add(x, y):
    """这是加法函数的文档"""
    return x + y


print(f"{add.__name__}'s doc {add.__doc__}")
print("==" * 20)
print(help(add))

images/7.函数用法/Pasted-image-20251004160751.png

装饰器的文档字符串问题

import datetime
import time


def logger(fn):
    def wrapper(*args, **kwargs):
        """wrapper's doc~~~"""
        print("调用前增强")
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        print("调用后增强")
        delta = (datetime.datetime.now() - start).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    return wrapper


@logger  # 等价于 add = logger(add) <=> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(f"{add.__name__}'s doc {add.__doc__}")

images/7.函数用法/Pasted-image-20251004161147.png

被装饰后,发现 add 的函数名和文档都变了。如何解决?

函数也是对象,特殊属性也是属性,也可以被覆盖。现在访问 add 函数实际上是在访问 wrapper 函数,所以使用原来定义的 add 函数的名称和文档属性覆盖 wrapper 的对应属性就可以了。

import time
import datetime


def logger(fn):
    def wrapper(*args, **kwargs):
        """wrapper's doc~~~"""
        print("调用增强前")
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        print("调用增强后")
        delta = (datetime.datetime.now() - start).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    def copy_properties(src, dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__

    copy_properties(fn, wrapper)
    return wrapper


@logger  # 等价于 add = logger(add) <==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

images/7.函数用法/Pasted-image-20251004161334.png

如果 copy_properties 是公用的函数,可以定义成全局的。实际上,这个函数很通用,基本上装饰器都会有这个问题。

带参装饰器

能否把 copy_properties 改成装饰器?这个装饰器是带参装饰器。

首先将 copy_properties 提取成全局函数。

import time
import datetime


def copy_properties(src, dst):
    dst.__name__ = src.__name__
    dst.__doc__ = src.__doc__


def logger(fn):
    def wrapper(*args, **kwargs):
        """wrapper's doc~~~"""
        print("调用增强前")
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)  # 参数解构
        print("调用增强后")
        delta = (datetime.datetime.now() - start).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    copy_properties(fn, wrapper)
    return wrapper


@logger  # 等价于 add = logger(add) <==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

因为装饰器只能是一个等效的单参函数,所以对 copy_properties 进行柯里化。

def copy_properties(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    return _copy

分析以下代码,为什么调用函数可以正常运行,而写成装饰器语法后就报错了?

import time
import datetime


def copy_properties(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    return _copy

def logger(fn):
    # @copy_properties(fn)
    def wrapper(*args, **kwargs):
        """wrapper's doc~~~"""
        print("调用增强前")
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)  # 参数解构
        print("调用增强后")
        delta = (datetime.datetime.now() - start).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    copy_properties(fn)(wrapper)
    return wrapper


@logger  # 等价于 add = logger(add) <==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

images/7.函数用法/Pasted-image-20251004163139.png

images/7.函数用法/Pasted-image-20251004163301.png

images/7.函数用法/Pasted-image-20251004163219.png

分析:

一定要注意装饰器的等价式:

def logger(fn):
    @copy_properties(fn)  # 等价于 wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args, **kwargs):
        pass

装饰器做了两件事:一是调用函数,二是将函数调用后的返回值赋给变量 wrapper

images/7.函数用法/Pasted-image-20251004163532.png

由于 _copy 函数没有返回值,所以默认返回 None。即 wrapper = None

正确实现:

import time
import datetime


def copy_properties(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst  # 这一句返回特别重要

    return _copy


def logger(fn):
    @copy_properties(fn)  # 等价于 wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args, **kwargs):
        """wrapper's doc~~~"""
        print("调用增强前")
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)  # 参数解构
        print("调用增强后")
        delta = (datetime.datetime.now() - start).total_seconds()
        print(f"Function {fn.__name__} took {delta}s")
        return res

    return wrapper


@logger  # 等价于 add = logger(add) <==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

@copy_properties(fn) 这种在装饰器后面跟着参数的装饰器称为带参装饰器

logger 设定一个阈值,对执行时长超过阈值的记录一下。

import time
import datetime


def copy_properties(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst

    return _copy


def logger(duration):
    def _looger(fn):
        @copy_properties(fn)  # 等价于 wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args, **kwargs):
            """wrapper's doc~~~"""
            print("调用增强前")
            start = datetime.datetime.now()
            res = fn(*args, **kwargs)  # 参数解构
            print("调用增强后")
            delta = (datetime.datetime.now() - start).total_seconds()
            print(f"Function {fn.__name__} took {delta}s {'Slow' if delta > duration else 'Fast'}")
            return res

        return wrapper

    return _looger


@logger(5)  # 等价于 add = logger(5)(add) ==> add = _looger(add) ==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

add(4, 5)

images/7.函数用法/Pasted-image-20251004163908.png

进一步提取记录功能,因为有可能输出到控制台,也可能写入日志,这个由一个函数提供。

import time
import datetime


def copy_properties(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst

    return _copy


def output(name, delta):
    print("写入文件")
    print(f"Function {name} took {delta:.2f}s Slow")


def logger(duration, output=lambda name, delta: print(f"Function {name} took {delta:.2f}s Slow")):
    def _looger(fn):
        @copy_properties(fn)  # 等价于 wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args, **kwargs):
            """wrapper's doc~~~"""
            print("调用增强前")
            start = datetime.datetime.now()
            res = fn(*args, **kwargs)  # 参数解构
            print("调用增强后")
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                output(fn.__name__, delta)
            return res

        return wrapper

    return _looger


@logger(1, output)  # 等价于 add = logger(1, output)(add) ==> add = _looger(add) ==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

add(4, 5)

images/7.函数用法/Pasted-image-20251004164236.png

属性更新

上例中 copy_properties 是通用功能,标准库 functools 已经提供了。

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
  • 类似我们全局 copy_properties 函数的功能,不是装饰器版本。
  • wrapper 包装函数、被更新者;wrapped 被包装函数、数据源。
  • 元组 WRAPPER_ASSIGNMENTS 中是要被覆盖的属性,有模块名 __module__,名称 __name__,限定名 __qualname__,文档 __doc__,参数注解 __annotations__
  • 元组 WRAPPER_UPDATES 中是要被更新的属性,__dict__ 属性字典。
  • 增加一个 __wrapped__ 属性,保留着 wrapped 函数。
import time
import datetime
import functools


# def copy_properties(src):
#     def _copy(dst):
#         dst.__name__ = src.__name__
#         dst.__doc__ = src.__doc__
#         return dst
#
#     return _copy


def logger(duration, output=lambda name, delta: print(f"Function {name} took {delta:.2f}s Slow")):
    def _looger(fn):
        # @copy_properties(fn)  # 等价于 wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args, **kwargs):
            """wrapper's doc~~~"""
            print("调用增强前")
            start = datetime.datetime.now()
            res = fn(*args, **kwargs)  # 参数解构
            print("调用增强后")
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                output(fn.__name__, delta)
            return res

        # copy_properties(fn)(wrapper)
        functools.update_wrapper(wrapper, fn)
        return wrapper

    return _looger


@logger(1)  # 等价于 add = logger(5)(add) ==> add = _looger(add) ==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

add(4, 5)

functools 模块提供了一个 wraps 装饰器函数,本质上调用的是 update_wrapper,它就是一个属性复制函数。

wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
  • 缺少了 wrapper 参数。
import time
import datetime
import functools
from functools import wraps


# def copy_properties(src):
#     def _copy(dst):
#         dst.__name__ = src.__name__
#         dst.__doc__ = src.__doc__
#         return dst
#     return _copy


def logger(duration, output=lambda name, delta: print(f"Function {name} took {delta:.2f}s Slow")):
    def _looger(fn):
        @wraps(fn)  # 本质上还是调用的 update_wrapper wrapper = wraps(fn)(wrapper)
        def wrapper(*args, **kwargs):
            """wrapper's doc~~~"""
            print("调用增强前")
            start = datetime.datetime.now()
            res = fn(*args, **kwargs)  # 参数解构
            print("调用增强后")
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                output(fn.__name__, delta)
            return res

        return wrapper

    return _looger


@logger(1)  # 等价于 add = logger(5)(add) ==> add = _looger(add) ==> add = wrapper
def add(x, y):
    """add doc~~~"""
    time.sleep(2)
    return x + y


print(add.__name__, add.__doc__)

add(4, 5)

总结:

  • 带参装饰器 @ 之后不是一个单独的标识符,是一个函数调用。
  • 函数调用的返回值又是一个函数,此函数是一个无参装饰器。
  • 带参装饰器,可以有任意个参数:
    • @func()
    • @func(1)
    • @func(1, 2[,…])
注意
> `@func()` 和 `@func` 不一样,前面还是一个带参装饰器。 > > `@func()` 等价于 `func()(wrapper)`。 > > `@func` 等价于 `func(wrapper)`。

测试题

import datetime
from functools import wraps


def logger(fn):
    @wraps(fn)  # 用被包装函数 fn 的属性覆盖包装函数 wrapper 的同名属性
    def wrapper(*args, **kwargs):  # wrapper wraps(fn)(wrapper)
        """wrapper's doc"""
        start = datetime.datetime.now()
        ret = fn(*args, *kwargs)  # 参数解构
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {took {}s.'.format(fn.__name_, delta))
        return ret

    return wrapper


@logger  # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add function"""
    pass


@logger
def sub(x, y):
    """sub function"""
    pass


print(add.__name__, sub.__name__)
`add` 函数、`sub` 函数执行过吗?`logger` 什么时候执行?`logger` 执行过几次?`wraps` 装饰器执行过几次?

`add` 和 `sub` 函数没有执行过。
> `logger` 在 `add` 和 `sub` 函数定义后立即执行。 > `logger` 执行过两次。 > `wraps` 装饰器执行过两次。
`wrapper` 的 `__name__` 等属性被覆盖过几次?

被覆盖过两次。在 `add` 和 `sub` 函数定义后 `logger` 函数会立即执行,入会就会定义 `wrapper` 函数,在 `wrapper` 函数定义好后 `wraps` 函数立即执行,将 `wrapper` 的 `__name__` 等属性覆盖。
`add.__name__` 打印什么名称?`sub.__name__` 打印什么名称?

add、sub。因为 `wraps` 装饰器会将 `wrapper` 的 `__name__` 等属性覆盖。
> > ![images/7.函数用法/Pasted-image-20251004172331.png](https://img2024.cnblogs.com/blog/3786934/202604/3786934-20260411020011482-1733192801.png)

26.7 内置装饰器

我们在面向对象学习时,学习过三种装饰器:propertystaticmethodclassmethod

property 装饰器

property 装饰器用于类中的函数,使得我们可以像访问属性一样来获取一个函数的返回值。

示例:prperty 装饰器的使用。

class User:
    def __init__(self, name, month_salary):
        self.name = name
        self.month_salary = month_salary

    @property
    def year_salary(self):
        return int(self.month_salary) * 12


if __name__ == '__main__':
    u1 = User("xln", "30000")
    print(f"年薪:{u1.year_salary}")

images/7.函数用法/Pasted-image-20250404004428.png

staticmethod 装饰器

staticmethod 装饰器同样是用于类中的方法,这表示这个方法将会是一个静态方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有 self 参数,也无法访问实例化后的对象。

示例:staticmethod 装饰器的使用。

class Person:
    @staticmethod
    def say_hello():
        print("hello world!")


if __name__ == '__main__':
    Person.say_hello()

images/7.函数用法/Pasted-image-20250404004454.png

classmethod 装饰器

classmethod 这个方法是一个类方法。该方法无需实例化,没有 self 参数。相对于 staticmethod 的区别在于它会接收一个指向类本身的 cls 参数。

示例:classmethod 装饰器。

class Person:
    @classmethod
    def say_hello(cls):
        print(f"我是{cls.__name__}")
        print("hello world!")


if __name__ == '__main__':
    Person.say_hello()

images/7.函数用法/Pasted-image-20250404004602.png

26.8 类装饰器

上面写的装饰器都是函数来完成的。我们也可以用类实现装饰器。

类能实现装饰器的功能,是由于当我们调用一个对象时,实际上调用的是它的 __call__ 方法。

示例:调用对象,__call__ 方法的使用。

class Demo:
    def __call__(self):
        print('我是 Demo')


demo = Demo()
demo()  # 直接调用对象,实质是调用了他的 __call__()

images/7.函数用法/Pasted-image-20250404004626.png

示例:类装饰器的使用案例。

class MyLogDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("日志纪录...")
        return self.func(*args, **kwargs)


@MyLogDecorator  # 等价于 fun2 = MyLogDecorator(fun2)
def fun2():
    print("使用功能2")


if __name__ == '__main__':
    fun2()

images/7.函数用法/Pasted-image-20250404005543.png

26.9 缓存装饰器和计时装饰器综合练习

示例:实现函数执行结果缓存和计时的装饰器功能。

import time


class CacheDecorator:
    __cache = {}

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # 如果缓存中有对应的方法名,则直接返回对应的返回值
        if self.func.__name__ in CacheDecorator.__cache:
            return CacheDecorator.__cache[self.func.__name__]
            # 如果缓存中没有对应的方法名,则进行计算,并将结果缓存
        else:
            result = self.func(*args, **kwargs)

        CacheDecorator.__cache[self.func.__name__] = result
        return result


def cost_time(func):
    def infunc(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"耗时:{end - start}")
        return result

    return infunc


@cost_time
@CacheDecorator
def func1_long_time():
    """模拟耗时较长,每次执行返回结果都一样的情况"""
    print("start func1")
    time.sleep(3)
    print("end func1")
    return 999


if __name__ == '__main__':
    print(func1_long_time())
    print("=" * 40)
    print(func1_long_time())

images/7.函数用法/Pasted-image-20251004173508.png

27 注解 annotation

Python 是动态语言,变量可以随时被赋值并改变类型,也就是说 Python 的变量是运行时决定的。

def add(x, y):
    return x + y


print(add(4, 5))
print(add('mag', 'edu'))
print(add([16], [11]))

print(add(4, 'abc'))  # 不到运行时,无法判断类型是否正确

动态语言缺点:

  1. 难发现:由于不能做任何类型检查,往往到了运行时问题才显现出来,或到了线上运行时才暴露出来。
  2. 难使用:函数使用者看到函数时,并不知道函数设计者的意图,如果没有详尽的文档,使用者只能猜测数据的类型。对于一个新的 API,使用者往往不知道该怎么使用,对于返回的类型更是不知道该怎么使用。

动态类型对类型的约束不强,在小规模开发的危害不大,但是随着 Python 的广泛使用,这种缺点确实对大项目的开发危害非常大。

  1. 文档字符串。对函数、类、模块使用详尽的使用描述、举例,让使用者使用帮助就能知道使用方式。但是,大多数项目管理不严格,可能文档不全,或者项目发生变动后,文档没有更新等等。
  2. 类型注解:函数注解、变量注解。

27.1 函数注解

def add(x: int, y: int) -> int:
    """
    
    :param x: 
    :param y: 
    :return: 
    """
    return x + y


help(add)
print(add(4, 5))
print(add('mag', 'edu'))

images/7.函数用法/Pasted-image-20251004174049.png

函数注解:

  • Python3.5 版本引入。
  • 对函数的形参和返回值类型的说明。
  • 只是对函数形参类型、返回值类型做的辅助的说明,非强制类型约束。
  • 第三方工具,例如 Pycharm 就可以根据类型注解进行代码分析,发现隐藏的 Bug。
  • 函数注解存放在函数的属性 __annotations__ 中,字典类型。
def add(x: int, y: int) -> int:
    """

    :param x:
    :param y:
    :return:
    """
    return x + y


print(add.__annotations__)

images/7.函数用法/Pasted-image-20251004174344.png

27.2 类型注解

i: int = 3
j: str = 'abc'
k: str = 300  # 非强制性约束

print(i, j, k)

images/7.函数用法/Pasted-image-20251004174604.png

类型注解:

  • 3.6 版本引入。
  • 对变量类型的说明,非强制约束。
  • 第三方工具可以进行类型分析和推断。

27.3 类型检查

函数传参经常传错,如何检查?

可以在函数内部写 isinstance 来判断参数类型是否正确,但是检查可以看做不是业务代码,写在里面就是侵入式代码。那如何更加灵活的检查呢?

  • 非侵入式代码。
  • 动态获取待检查的函数的参数类型注解。
  • 当函数调用传入实参时,和类型注解比对。
def check(fn):
    print(fn.__annotations__)


def add(x: int, y: int) -> int:
    """
    加法函数

    :param x: int
    :param y: int
    :return: int
    """
    # 这种检查是通用的功能,是 n 个函数都可以拥有的功能,
    # 而且检查是非业务代码 —— 典型的装饰器该做的
    # 检查的方案,硬编码,如果有变化就要修改
    if isinstance(x, int) and isinstance(y, int):
        r = x + y
        return r
    else:
        raise TypeError  # built-in 加载时,加载内建函数,异常类


# 字典,所有参数和返回值的类型注解 update_wrapper assigned
# 字典,可变类型,字典 3.6 之后记录了录入序,但字典 key 一定要知道是无序的
print(add.__annotations__)
check(add)

print(add('abc', 'xyz'))  # 如何检查实参类型对不对

images/7.函数用法/Pasted-image-20251004175207.png

能使用函数的 __annotations__ 属性吗?虽然 Python3.6 之后,字典记录了录入序,但是我们还是要认为字段是无顺序的。那如何和按位置传的实参对应呢?

使用 inspect 模块

27.4 inspect 模块

inspect 模块可以获取 Python 中各种对象信息。

  • inspect.isfunction(add),是否是函数。
  • inspect.ismethod(pathlib.Path().absolute),是否是类的方法,要绑定。
  • inspect.isgenerator(add),是否是生成器对象。
  • inspect.isgeneratorfunction(add),是否是生成器函数。
  • inspect.isclass(add),是否是类。
  • inspect.ismodule(inspect),是否是模块。
  • inspect.isbuiltin(print),是否是内建对象。

还有很多 is 函数,需要的时候查阅 inspect 模块帮助。

inspect.signatuer(callable, * , follow_wrapper=True)
  • 获取可调用对象的签名。
  • Python3.5 增加 follow_wrapped,如果使用 functoolswrapsupdate_wrapper,则 follow_wrappedTrue,则获取包装函数的 __wrapped__ 属性中的函数签名,也就是去获取真正的被包装函数的签名。
import inspect


def add(x: int, y: int) -> int:
    """
    加法函数

    :param x: int
    :param y: int
    :return: int
    """
    # 这种检查是通用的功能,是 n 个函数都可以拥有的功能,
    # 而且检查是非业务代码 —— 典型的装饰器该做的
    # 检查的方案,硬编码,如果有变化就要修改
    if isinstance(x, int) and isinstance(y, int):
        r = x + y
        return r
    else:
        raise TypeError  # built-in 加载时,加载内建函数,异常类


sig = inspect.signature(add)  # 获得函数对象的签名信息

print(f"sig——{sig}")
# 属性,所有的参数;OrderedDict 有序的字典,记录了 kv 的录入序
print(f"sig.parameters——{sig.parameters}")
# ('x', <Parameter "x: int">)(x 形参的名称字符串,Parameter类型的对象)
print("=" * 40)

params = sig.parameters  # 有序字典
print(params)

for k, v in params.items():
    print(type(k), k)
    print(type(v), v.name, v.default, v.kind, v.annotation)

images/7.函数用法/Pasted-image-20251004182150.png

inspect.Parameter 类:

  • 4 个属性
    1. name,参数名,字符串。
    2. default,缺省值。
    3. annotation,类型注解。
    4. kind,类型。
      • POSITIONAL_ONLY,只接受位置传参。
      • POSITIONAL_OR_KWWORD,可接受关键字或位置传参。
      • VAR_POSITIONAL,可变位置参数,对应 *args
      • KEYWORD_ONLY,对应 **args 之后出现的非可变关键字形参,只接受关键字传参。
      • VAR_KEYWORD,可变关键字参数,对应 **kwargs
  • empty,特殊类,用来标记 default 属性或者 annotation 属性为空,因为 None 也是一种类型,无法用于标记默认值或注解为空。
import inspect


def add(x: int, /, y: int = 5, *args, m=6, n, **kwargs) -> int:
    return x + y + m + n


sig = inspect.signature(add)  # 获取签名
print(sig)
print(sig.return_annotation)  # 返回值注解
print("=" * 40)

params = sig.parameters  # 所有参数
print(type(params))
print(params)  # 有序字典 OrderedDict
print("=" * 40)

for k, v in params.items():
    print(type(k), k)
    t: inspect.Parameter = v  # 这一步是多余的,但是 t 使用了变量注解
    print(t.name, t.default, t.kind, t.annotation, sep='\t')
    print('-' * 30)

images/7.函数用法/Pasted-image-20251004182946.png

27.5 参数类型检查

有以下函数:

def add(x, y: int = 7) -> int:
    return x + y


add(4, 5)
add('msg', 'edu')

请检查用户的输入是否符合参数类型注解的要求。

分析:

  • 调用时,用户才会传入实参,才能判断实参是否符合类型要求。
  • 调用时,让用户感觉上还是调用原函数。
  • 如果类型不符,提示用户。

先实现对 add 函数的参数类型的提取。

import inspect


def add(x: int, y: int) -> int:
    return x + y


def check(fn):
    sig = inspect.signature(fn)
    params = sig.parameters

    for k, v in params.items():
        print(k, v.kind, v.annotation)


check(add)
# 怎么检查参数的类型
print(add(4, 5))

如何解决 add(4, 5) 调用问题?

import inspect
from functools import wraps


def check(fn):
    @wraps(fn)  # 等价于 wrapper = wraps(fn)(wrapper) <==> wrapper = wrapper
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters
        print(args, kwargs)
        print(params)

        res = fn(*args, **kwargs)

        return res

    return wrapper


@check  # 等价于 add = check(add) <==> add = wrapper
def add(x: int, y: int = 7) -> int:
    return x + y


print(add(4, 5))

对于按位置传参如何解决?

import inspect
from functools import wraps


def check(fn):
    @wraps(fn)  # 等价于 wrapper = wraps(fn)(wrapper) <==> wrapper = wrapper
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters

        values = tuple(params.values())

        for i, v in enumerate(args):
            if values[i].annotation is not values[i].empty and isinstance(v, values[i].annotation):
                print(f"{values[i].name}={v} is ok")
            else:
                raise TypeError

        res = fn(*args, **kwargs)

        return res

    return wrapper


@check  # 等价于 add = check(add) <==> add = wrapper
def add(x: int, y: int = 7) -> int:
    return x + y


print(add(4, 5))

对于关键字传参怎么解决?

import inspect
from functools import wraps


def check(fn):
    @wraps(fn)  # 等价于 wrapper = wraps(fn)(add) <==> wrapper = wrapper
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters

        values = tuple(params.values())

        for i, v in enumerate(args):
            if values[i].annotation is not values[i].empty and not isinstance(v, values[i].annotation):
                raise TypeError

        for k, v in kwargs.items():
            if params[k].annotation is not inspect._empty and not isinstance(v, params[k].annotation):  # inspect._empty 等价于 params[k].empty
                raise TypeError

        res = fn(*args, **kwargs)

        return res

    return wrapper


@check  # 等价于 add = check(add) <==> add = wrapper
def add(x: int, y: int = 7) -> int:
    return x + y

进一步进行返回值类型的检查:sig.return_annotation 获取返回值的类型注解。

import inspect
from functools import wraps


def check(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters

        values = tuple(params.values())

        for i, v in enumerate(args):
            if values[i].annotation is not values[i].empty and not isinstance(v, values[i].annotation):
                raise TypeError

        for k, v in kwargs.items():
            if params[k].annotation is not params[k].empty and not isinstance(v, params[k].annotation):
                raise TypeError

        res = fn(*args, **kwargs)

        # 进行返回值类型检查
        if sig.return_annotation is not sig.empty and not isinstance(res, sig.return_annotation):
            raise TypeError

        return res

    return wrapper


@check
def add(x: int, y: int = 7) -> int:
    return x + y


print(add(4, y=5))

28 functools 模块

28.1 reduce

  • 就是减少的意思,将多个对象通过某种迭代最后合成一个对象。
  • 初始值没有提供就在可迭代对象中取一个。

格式:

functools.reduce(function, iterable[, initial])
参数 介绍
function 操作函数。
iterable 可迭代对象。
initial 初始值,如果设置了 initial 则第一个参数第一次就为其,若没有设置 initial 则将第一个参数第一次的值设为可迭代对象的第一个值。
from functools import reduce

s = sum(range(10))
print(s)

s = reduce(lambda x: x, range(10))
print(s)  # TypeError: <lambda>() takes 1 positional argument but 2 were given

images/7.函数用法/Pasted-image-20251004185419.png

从上面的异常推断 lambda 应该为 2 个参数。

from functools import reduce

print(reduce(lambda x, y: print(x, y), range(10)))

images/7.函数用法/Pasted-image-20251004202348.png

from functools import reduce

print(reduce(lambda x, y: (x, y), range(10)))

images/7.函数用法/Pasted-image-20251004202646.png

上一次 lambda 函数返回值会成为下一次的 x

from functools import reduce

s = sum(range(10))
print(s)

s = reduce(lambda x, y: x + y, range(10))
print(s)

s = reduce(lambda x, y: x + y, range(10))
print(s)

images/7.函数用法/Pasted-image-20251004202804.png

sum 只能求和,reduce 能做更加复杂的迭代计算。

28.2 partial

偏函数:

  • 把函数部分参数固定下来,相当于为部分的参数添加了固定的默认值,形成一个新的函数,并返回这个新函数。
  • 这个新函数是对原函数的封装。
from functools import partial


def add(x, y):
    return x + y


newadd = partial(add, y=5)
print(newadd(4))  # 等价于调用 add(4, y=5)
print(newadd(4, y=15))  # 等价于调用 add(4, y=15)
print(newadd(x=4))  # 等价于调用 add(x=4, y=5)
print(newadd(4, 6))  # 可以吗?等价于调用 add(4, 6, y=5) ==》 y 重复赋值
print(newadd(y=6, x=4))  # 等价于调用 add(x=4, y=6)

看等价函数:

import inspect

print(inspect.signature(newadd))

images/7.函数用法/Pasted-image-20251004203402.png

from functools import partial


def add(x, y, *args):
    return x + y + sum(args)


newadd = partial(add, 1, 2, 3, 4, 5)

print(newadd())  # 等价于调用 add(1, 2, 3, 4, 5)
print(newadd(1))  # 等价于调用 add(1, 2, 3, 4, 5, 1)
print(newadd(1, 2)) # 等价于调用 add(1, 2, 3, 4, 5, 1, 2)
print(newadd(x=1))  # 等价于调用 add(1, 2, 3, 4, 5, x=1) x 重复赋值
print(newadd(x=1, y=2))  # 等价于 add(1, 2, 3, 4, 5, x=1, y=2) x,y 重新赋值

看等价函数:

import inspect

print(inspect.signature(newadd))

images/7.函数用法/Pasted-image-20251004212122.png

partial 本质

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):  # 包装函数
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)

    newfunc.func = func  # 保留原函数
    newfunc.args = args  # 保留原函数的位置参数
    newfunc.keywords = keywords  # 保留原函数的关键字参数参数
    return newfunc


def add(x, y):
    return x + y


foo = partial(add, 4)
foo(5)

尝试分析 functools.wraps 的实现。类比下面的实现。

from functools import partial, wraps
import inspect


def add(a, b, c, d):
    return a + b + c + d


newadd = partial(add, b=2, c=3, d=4)
print(inspect.signature(newadd))

28.3 lru_cache

@functools.lru_cache(maxsize=128, typed=False)
  • lru 即 Least--recently-used,最近最少使用。cache 缓存。

  • 如果 maxsize 设置为 None,则禁用 LRU 功能,并且缓存可以无限制增长。当 maxsize 是二的幂时,LRU 功能执行得最好。

  • 如果 typed 设置为 True,则不同类型的函数参数将单独缓存。例如,f(3)f(3.0) 将被视为具有不同结果的不同调用。

  • Python3.8 简化了调用,可以使用无参装饰器的方式来调用 lru_cache,但是 lru_cache 本质上是一个有参装饰器。

    from functools import lru_cache
    
    @lru_cache
    def add():
        pass
    
    # 等价于
    @lru_cache(128)
    def add():
        pass
    
from functools import lru_cache
import time


@lru_cache()
def add(x, y=5):
    print('-' * 30)
    time.sleep(3)
    return x + y


print(1, add(4, 5))
print(2, add(4, 5))
print(3, add(4.0, 5))  # typed 为 True 与 1, 2 不等效
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add(4))
print(7, add(4, y=5))
print(8, add(x=4))

images/7.函数用法/image-20221023113823327.png

到底什么调用才能用缓存呢?

lru_cache 本质

  • 内部使用了一个字典
  • key 是由 _make_key 函数构造出来
from functools import _make_key

print(_make_key((4, 5), {}, False))
print(_make_key((4,), {'y': 5}, False))
print(_make_key((), {'x': 4, 'y': 5}, False))
print(_make_key((4, 5), {}, True))

images/7.函数用法/image-20221023171615584.png

images/7.函数用法/image-20221023171627042.png

详细分析

from functools import lru_cache
import time


@lru_cache()
def add(x, y=5):
    print('-' * 30)
    time.sleep(3)
    return x + y


print(1, add(4, 5))
print(2, add(4, 5))
print(3, add(4.0, 5))  # typed 为 True 与 1, 2 不等效
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add(4))
print(7, add(4, y=5))
print(8, add(x=4))

点击到 lru_cache 源代码:

def lru_cache(maxsize=128, typed=False):
    """Least-recently-used cache decorator.

    If *maxsize* is set to None, the LRU features are disabled and the cache
    can grow without bound.

    If *typed* is True, arguments of different types will be cached separately.
    For example, f(3.0) and f(3) will be treated as distinct calls with
    distinct results.

    Arguments to the cached function must be hashable.

    View the cache statistics named tuple (hits, misses, maxsize, currsize)
    with f.cache_info().  Clear the cache and statistics with f.cache_clear().
    Access the underlying function with f.__wrapped__.

    See:  http://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)

    """

    # Users should only access the lru_cache through its public API:
    #       cache_info, cache_clear, and f.__wrapped__
    # The internals of the lru_cache are encapsulated for thread safety and
    # to allow the implementation to change (including a possible C version).

    if isinstance(maxsize, int):
        # Negative maxsize is treated as 0
        if maxsize < 0:
            maxsize = 0
    elif callable(maxsize) and isinstance(typed, bool):
        # The user_function was passed in directly via the maxsize argument
        user_function, maxsize = maxsize, 128
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)
    elif maxsize is not None:
        raise TypeError(
            'Expected first argument to be an integer, a callable, or None')

    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)

    return decorating_function

首先看 lru_cache 作为有参装饰器怎么执行

@lru_cache()
def add(x, y=5):
    pass

此时 @lru_cache() 等价于 add = lru_cache()(add)

首先执行 lru_cache()

images/7.函数用法/image-20221027001324307.png

着执行 decorating_function(add)

images/7.函数用法/image-20221027002144170.png

此时 add = wrapper

为什么 lru_cache 可以当做无参装饰器?

@lru_cache
def add(x, y=5):
    pass

此时 @lru_cache 等价于 add = lru_cache(add)

images/7.函数用法/image-20221027000313394.png

此时 add = wrapper

wrapper 函数由一个被保护的函数 _lru_cache_wrapper 生成。那么 _lru_cache_wrapper 中做了什么?

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    # Constants shared by all lru cache instances:
    sentinel = object()          # unique object used to signal cache misses
    make_key = _make_key         # build a key from the function arguments
    PREV, NEXT, KEY, RESULT = 0, 1, 2, 3   # names for the link fields

    cache = {}
    hits = misses = 0
    full = False
    cache_get = cache.get    # bound method to lookup a key or return None
    cache_len = cache.__len__  # get cache size without calling len()
    lock = RLock()           # because linkedlist updates aren't threadsafe
    root = []                # root of the circular doubly linked list
    root[:] = [root, root, None, None]     # initialize by pointing to self

    if maxsize == 0:

        def wrapper(*args, **kwds):
            # No caching -- just a statistics update
            nonlocal misses
            misses += 1
            result = user_function(*args, **kwds)
            return result

    elif maxsize is None:

        def wrapper(*args, **kwds):
            # Simple caching without ordering or size limit
            nonlocal hits, misses
            key = make_key(args, kwds, typed)
            result = cache_get(key, sentinel)
            if result is not sentinel:
                hits += 1
                return result
            misses += 1
            result = user_function(*args, **kwds)
            cache[key] = result
            return result

    else:

        def wrapper(*args, **kwds):
            # Size limited caching that tracks accesses by recency
            nonlocal root, hits, misses, full
            key = make_key(args, kwds, typed)
            with lock:
                link = cache_get(key)
                if link is not None:
                    # Move the link to the front of the circular queue
                    link_prev, link_next, _key, result = link
                    link_prev[NEXT] = link_next
                    link_next[PREV] = link_prev
                    last = root[PREV]
                    last[NEXT] = root[PREV] = link
                    link[PREV] = last
                    link[NEXT] = root
                    hits += 1
                    return result
                misses += 1
            result = user_function(*args, **kwds)
            with lock:
                if key in cache:
                    # Getting here means that this same key was added to the
                    # cache while the lock was released.  Since the link
                    # update is already done, we need only return the
                    # computed result and update the count of misses.
                    pass
                elif full:
                    # Use the old root to store the new key and result.
                    oldroot = root
                    oldroot[KEY] = key
                    oldroot[RESULT] = result
                    # Empty the oldest link and make it the new root.
                    # Keep a reference to the old key and old result to
                    # prevent their ref counts from going to zero during the
                    # update. That will prevent potentially arbitrary object
                    # clean-up code (i.e. __del__) from running while we're
                    # still adjusting the links.
                    root = oldroot[NEXT]
                    oldkey = root[KEY]
                    oldresult = root[RESULT]
                    root[KEY] = root[RESULT] = None
                    # Now update the cache dictionary.
                    del cache[oldkey]
                    # Save the potentially reentrant cache[key] assignment
                    # for last, after the root and links have been put in
                    # a consistent state.
                    cache[key] = oldroot
                else:
                    # Put result in a new link at the front of the queue.
                    last = root[PREV]
                    link = [last, root, key, result]
                    last[NEXT] = root[PREV] = cache[key] = link
                    # Use the cache_len bound method instead of the len() function
                    # which could potentially be wrapped in an lru_cache itself.
                    full = (cache_len() >= maxsize)
            return result

    def cache_info():
        """Report cache statistics"""
        with lock:
            return _CacheInfo(hits, misses, maxsize, cache_len())

    def cache_clear():
        """Clear the cache and cache statistics"""
        nonlocal hits, misses, full
        with lock:
            cache.clear()
            root[:] = [root, root, None, None]
            hits = misses = 0
            full = False

    wrapper.cache_info = cache_info
    wrapper.cache_clear = cache_clear
    return wrapper

images/7.函数用法/image-20221027004321881.png

images/7.函数用法/image-20221027005119690.png

此时 add 实际上是 _lru_cache_wrapper 中定义的 wrapper 函数。调用 wrapper 函数,相当于调用 wrapper 函数。

wrapper 是怎样执行的?

下面分析 maxsize = None 时的 wrapper 函数。wrapper 原理都差不多,只不过有大小限制的 wrapper 函数中多了进程处理和 cache 存满的操作,我们不需要全部学习,只需要把核心搞定就行。

def wrapper(*args, **kwds):
    # Simple caching without ordering or size limit
    nonlocal hits, misses
    key = make_key(args, kwds, typed)
    result = cache_get(key, sentinel)
    if result is not sentinel:
        hits += 1
        return result
    misses += 1
    result = user_function(*args, **kwds)
    cache[key] = result
    return result

images/7.函数用法/image-20221027012155241.png

make_key 是怎么创建键的?

def _make_key(args, kwds, typed,
             kwd_mark = (object(),),
             fasttypes = {int, str},
             tuple=tuple, type=type, len=len):
    """Make a cache key from optionally typed positional and keyword arguments

    The key is constructed in a way that is flat as possible rather than
    as a nested structure that would take more memory.

    If there is only a single argument and its data type is known to cache
    its hash value, then that argument is returned without a wrapper.  This
    saves space and improves lookup speed.

    """
    # All of code below relies on kwds preserving the order input by the user.
    # Formerly, we sorted() the kwds before looping.  The new way is *much*
    # faster; however, it means that f(x=1, y=2) will now be treated as a
    # distinct call from f(y=2, x=1) which will be cached separately.
    key = args
    if kwds:
        key += kwd_mark
        for item in kwds.items():
            key += item
    if typed:
        key += tuple(type(v) for v in args)
        if kwds:
            key += tuple(type(v) for v in kwds.values())
    elif len(key) == 1 and type(key[0]) in fasttypes:
        return key[0]
    return _HashedSeq(key)

例如调用

add(4, y=5)

images/7.函数用法/image-20221027204447300.png

_make_key() 函数返回了一个列表,为什么可以作为字典的键?

images/7.函数用法/image-20221027204607754.png

images/7.函数用法/image-20221027204831358.png

images/7.函数用法/image-20221027204714437.png

注意:

print(isinstance(key, list))
print(type(key) == list)

images/7.函数用法/image-20221027205146835.png

【 以上结果表明 key 是 list 的子类,list 是不可 hash 的。返回的是 _HashedSeq 对象,一定是在 _HashedSeq 中做了处理。

images/7.函数用法/image-20221027205951032.png

应用

# 斐波那契数列 lru_cache版
from functools import lru_cache
import datetime


# 起始时间
start = datetime.datetime.now()

@lru_cache()
def fib(n):
    return 1 if n < 3 else fib(n - 1) + fib(n - 2)

print(f"耗时:{(datetime.datetime.now()-start).seconds}")

print(fib(101))

images/7.函数用法/image-20221023180929075.png

cache 用在什么场合?

用 kv 字典实现,比如 redis 数据库 kv

  1. 不变。

    恒久不变:

    一段时间内,不变化,

    给定一个值,返回一个恒久不变值;

    一段时间内,给定一个值,一定可以返回一个不变化的值。

    幂等性,给定一个输入,一定返回一个不变的结果。

    'abc' hash('abc')

  2. 计算代价高

    给一个输入,得到的结果需要时间长,可以用空间换时间,将输入作为 key -> value 字典

  3. 使用频度
    在一段时间内,使用频度高,空间换时间,kv

总结

lru_cache 装饰器应用

  • 使用前提
    1. 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样结果
    2. 计算代价高,函数执行时间很长
    3. 需要多次执行,每一次计算代价高
  • 本质是建立函数调用的参数到返回值的预测
  • 缺点
    1. 不支持缓存过期,key 无法过期、失效
    2. 不支持清除操作
    3. 不支持分布式,是一个单机的缓存
  • 适用场景,单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询
posted @ 2026-04-11 02:00  挖掘鱼  阅读(9)  评论(0)    收藏  举报