开启了新一篇章,之前的章节1-6都在讲数据结构,从这一新篇章开始,讲的是函数即对象。
初步介绍
Functions in Python are first-class objects. Programming language researchers define a “first-class object” as a program entity that can be:
• Created at runtime
• Assigned to a variable or element in a data structure
• Passed as an argument to a function
• Returned as the result of a function
迭代器
因为本文会碰到map等用到迭代器的方法,所以在此之前我们先将这个介绍清楚。
好的,我们现在进一步深入,将迭代器与 计算机系统中的内存分配和使用 联系起来,通过图解的形式描述内存是如何被迭代器利用的,并解释为什么它能保持高效。
1. 内存与数据存储的基础
在计算机中,内存的使用分为以下几个主要部分:
- 堆(Heap): 用于动态分配内存,如创建对象、存储数据(例如
list
或dict
的元素)。 - 栈(Stack): 用于存储函数调用时的局部变量、逻辑执行状态等。
- 代码段和常量区: 存储静态的代码和只读数据(如字符串字面量等)。
迭代器的特点:
迭代器不一次性把所有数据加载到 堆 或 栈 中,而是动态生成数据,仅使用栈或少量堆空间来存储计算上下文(比如当前索引、已生成的位置)。
2. range
的内存结构演示
代码:
r = range(1, 5)
it = iter(r) # 创建迭代器
print(next(it)) # 第一次调用,返回 1
print(next(it)) # 第二次调用,返回 2
内存示意图:整体层级
┌────────────────────┬──────────────────────────────────┬──────────────┐
│ 栈 (Stack) │ 堆 (Heap) │ 常量区 │
├────────────────────┼──────────────────────────────────┼──────────────┤
│ 局部变量: │ range对象: │ 字面量: │
│ r → range(1, 5) │ start = 1 │ "1" │
│ it → iter(r) │ stop = 5 │ "5" │
│ │ step = 1 │ │
│ │ 迭代器对象 iter(r): │ │
│ │ range → range(1, 5) │ │
│ │ current = 3 (动态变化) │ │
└────────────────────┴──────────────────────────────────┴──────────────┘
解释:
-
栈:
- 存储了局部变量
r
和it
。 r
指向了位于堆中的range
对象。it
是从r
派生的迭代器,其中存储了 当前状态current = 1
(当前应该生成的值)。
- 存储了局部变量
-
堆:
- 堆中有一个
range
对象,包含了生成规则(start=1
,stop=5
,step=1
)。 - 一个迭代器对象派生自
range
,它保存当前迭代的上下文信息(例如current=3
)。
- 堆中有一个
-
常量区:
- 存储了 Python 程序中定义的常量数据(比如
1
和5
)。
- 存储了 Python 程序中定义的常量数据(比如
内存动态更新过程(图解)
1. 初始状态:
范围值: range(1, 5)
栈:
[栈帧]
r -> range
it -> iter(r)
堆:
range对象: {start=1, stop=5, step=1}
迭代器对象: {current=1}
常量:
数字 1, 5
2. next(it)
第一次消费:
栈:
[栈帧]
局部变量: r, it
堆:
range对象: {start=1, stop=5, step=1}
迭代器对象: {current=2} # 更新为下一个状态
返回值: 1
- 调用
next(it)
:- 返回当前值:
current=1
。 - 更新上下文状态:
current
变为2
。
- 返回当前值:
3. next(it)
第二次消费:
栈:
[栈帧]
局部变量: r, it
堆:
range对象: {start=1, stop=5, step=1}
迭代器对象: {current=3}
返回值: 2
4. 为什么迭代器不存储整个数据?
- 内存中一直是 “规则 + 状态” 的描述(如
current=2
),永远不会将range
对应的值[1, 2, 3, 4]
全部加载到内存。 - 如果输出
[1, 2, 3, 4]
,这些值被用户端消费,随即从内存中丢弃。
3. 更复杂场景:文件迭代器的内存图解
代码:
with open('large_file.txt', 'r') as f:
for line in f:
print(line.strip())
内存示意图(文件迭代器与内存管理)
┌──────────────┬────────────────────┬──────────────┐
│ 栈 │ 堆 │ 常量区 │
├──────────────┼────────────────────┼──────────────┤
│ 局部变量:f │ 文件对象: │ 缓存的行数据 │
│ 指向文件指针 │ 文件指针位置: 105 │ 当前行 = “line1” │
│ │ 文件缓冲区: [line1] | │
└──────────────┴────────────────────┴──────────────┘
文件迭代的内存更新过程:
-
初始阶段:
- 文件缓冲区为空,文件指针指向文件开头位置。
- 内存中仅仅保存了文件对象。
-
第一次调用:
- 缓冲区加载了一部分文件内容(少量数据)。
- 文件指针更新到了下一行的开头位置。
- 数据消耗一行并输出结果,该行即被丢弃(不再占用内存)。
栈:
f -> 文件对象
堆:
文件对象: 缓冲区只存储 “line1” ,文件指针位于 line2 的开头
当前行: "line1"
- 逐渐迭代整个文件:
- 缓冲区内容按需加载。
- 逐行处理,丢弃之前已经使用的行。
- 文件指针逐渐扫描整个文件,提高效率,节省内存。
4. 无限数据流:内存表现
Python 的 itertools.count()
是一个经典的无限迭代器。其状态更新更简单,只需维护一个全局整数。
代码:
from itertools import count
# 无限迭代器
it = count(1, 2) # 从 1 开始,每次递增 2
内存结构:
栈:
it -> 迭代器对象
堆:
迭代器对象
当前值: current = 1
step = 2
迭代更新:
调用 next(it)
每次都会计算当前值 current
并递增 step
,只存储当前数量以及规则,完全避免了生成无限个数字的内存压力。
调用 next(it) → 返回 1, 更新为 3
调用 next(it) → 返回 3, 更新为 5
...
5. 总结:迭代器与内存管理之关键点
-
按需加载,动态生成:
迭代器只存储“当前状态”(上下文),而非整个数据集合,节省内存。 -
堆与栈的分工:
- 栈中存储局部变量,例如迭代器对象。
- 堆中存储核心上下文信息,例如
current
、缓冲区等。
-
常量区存储基础规则:
- 不会存储实际计算结果,仅保存生成规则。
-
大数据/无限数据流支持:
- 数据一旦被消费,就可以丢弃。
- 适合高效处理大文件、流数据或动态生成的序列。
1、Treating a Function Like an Object
一等函数的定义
一个语言中的函数被称为“一等函数”时,它需满足以下条件:
- 函数可以作为变量赋值。
- 函数可以作为参数传递给另一个函数。
- 函数可以作为函数的返回值。
Python 中的函数完全符合这些条件。
第一部分:函数是对象
例子分析 7-1:
定义了一个递归函数 factorial
(阶乘函数):
def factorial(n):
"""returns n!"""
return 1 if n < 2 else n * factorial(n - 1)
我们为 factorial
定义了一个文档字符串(docstring):"""returns n!"""
。
尝试执行以下操作:
>>> factorial(42)
1405006117752879898543142606244511569936384000000000 # 调用函数,计算 42 的阶乘
>>> factorial.__doc__
'returns n!' # 访问函数的 __doc__ 属性
>>> type(factorial)
<class 'function'> # 确认函数的类型
关键点:
-
函数是对象:
- 函数
factorial
是function
类的实例,可以被操作,例如调用 (factorial(42)
) 或访问属性(如factorial.__doc__
)。 - 使用
type(factorial)
可以看到它的类型是<class 'function'>
。
- 函数
-
函数的
__doc__
属性:- 存储了函数的文档字符串(docstring),在帮助文档中生成
help()
的输出内容。例如,输入help(factorial)
时,会显示returns n!
。
- 存储了函数的文档字符串(docstring),在帮助文档中生成
第二部分:函数的“一等性质”
例子分析 7-2:
-
将函数赋值给变量:
>>> fact = factorial >>> fact <function factorial at 0x...> >>> fact(5) 120
- 将
factorial
赋值给另一个变量fact
。此时,fact
与factorial
指向同一个函数对象。 - 我们可以像调用
factorial
一样调用fact
,返回结果 120(即5!
)。
- 将
-
将函数作为参数传递:
>>> map(factorial, range(11)) # 将函数应用于范围 0-10 的每个数 <map object at 0x...> >>> list(map(factorial, range(11))) [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
- 函数
map
接收两个参数:- 一个函数(
factorial
)。 - 一个可迭代对象(
range(11)
,即[0, 1, 2, ..., 10]
)。
- 一个函数(
- 它会将
factorial
应用到可迭代对象的每个元素。结果是每个数的阶乘组成的列表。
- 函数
补充例子
例子 1:函数作为返回值
我们定义一个函数,返回另一个函数:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
使用:
adder = outer_function(10) # 返回的 inner_function 中 x 被绑定为 10
result = adder(5) # 调用 inner_function,y = 5,结果 15
print(result) # 输出:15
例子 2:高阶函数
一个高阶函数是接收至少一个函数作为参数的函数。例如:
def apply_function(func, value):
return func(value)
squared = apply_function(lambda x: x**2, 4) # 将匿名函数传递给 apply_function
print(squared) # 输出:16
注意点总结
-
赋值不等于复制函数:
- 当我们将函数赋值给另一个变量时,并不是重新创建了该函数,而是创建了对原函数对象的引用。
- 即
fact
和factorial
指向的是同一个对象。
-
map
的返回值:map
返回的是一个映射对象,不是直接的列表。需要使用list()
来获取最终结果。
-
错误用法:
- 不要在函数名后面加
()
传递。例如map(factorial(), range(11))
是错误的,因为factorial()
会被立即执行而不是作为函数传递。
- 不要在函数名后面加
总结
将函数视为对象是 Python 函数式编程的基础。通过掌握函数的以下特性:
- 变量赋值:函数可以被赋值给变量。
- 参数传递:函数可以作为参数传递给其他函数。
- 返回值:函数可以作为其他函数的返回值。
我们能够编写更灵活、更强大的代码,并为后续深入了解高阶函数和函数式编程奠定基础。
2、Higher-Order Functions
1. 什么是高阶函数?
高阶函数的核心特点:
- 函数作为参数:将一个函数传递给另一个函数。
- 返回函数作为结果:一个函数可以创建并返回新的函数。
示例 1:内置函数 sorted
的参数 key
sorted
是 Python 的一个内置函数,用于排序。通过 key
参数,你可以传入一个函数作为排序的依据。
# 按单词长度排序
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=len))
# 输出:['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
解释:这里 len
函数被传递给 key
,意味着依据每个单词的长度来排序。
示例 2:按单词反转后的顺序排序
sorted
还可以用任意自定义的一参函数。例如,按单词反转后的结果排序:
def reverse(word):
return word[::-1] # 将单词反转
# 示例
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=reverse))
# 输出:['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
补充解释:
word[::-1]
是字符串切片,用于反转字符串。- 原始单词列表未改变,仅通过
key=reverse
指定的逻辑对排序进行了影响,结果根据反转后的单词顺序排序。
补充例子:排序整数列表时按平方值排序
numbers = [1, -3, 2, -5, 4]
print(sorted(numbers, key=lambda x: x**2))
# 输出:[1, 2, -3, 4, -5]
解析:
key=lambda x: x**2
指定排序依据为数字的平方值。- 这里引入了匿名函数
lambda
(后面会详细讲)。
2. 常见高阶函数及其现代替代方案
Python 提供了一些经典高阶函数:map
、filter
和 reduce
。
但随着 Python 演变,这三者常被列表推导式或其他内置函数替代,因为后者更具可读性和性能优势。
2.1 map
函数
- 作用:对序列中的每一个元素应用一个函数,返回结果的迭代器。
- 格式:
map(func, iterable)
示例:
from math import factorial
# 计算 0! 到 5! 的阶乘
print(list(map(factorial, range(6))))
# 输出:[1, 1, 2, 6, 24, 120]
更推荐的现代替代方法是列表推导式:
print([factorial(n) for n in range(6)])
# 输出:[1, 1, 2, 6, 24, 120]
2.2 filter
函数
- 作用:过滤序列,保留函数返回
True
的元素。 - 格式:
filter(func, iterable)
示例:
# 保留列表中奇数的阶乘
print(list(map(factorial, filter(lambda n: n % 2, range(6)))))
# 输出:[1, 6, 120]
用列表推导式更清晰:
print([factorial(n) for n in range(6) if n % 2])
# 输出:[1, 6, 120]
2.3 reduce
函数
reduce
用于将序列中的所有元素逐步聚合成一个值,从 Python 3 起被移至 functools
模块中。
- 作用:对序列的元素逐一应用指定的函数,以计算最终结果。
- 格式:
reduce(func, iterable)
示例:
from functools import reduce
from operator import add
# 计算 0 到 99 的累加和
print(reduce(add, range(100)))
# 输出:4950
现代替代方法是直接使用内置的 sum
函数,简单且高效:
print(sum(range(100)))
# 输出:4950
补充例子:计算列表内所有数字的乘积
from functools import reduce
from operator import mul
nums = [1, 2, 3, 4]
print(reduce(mul, nums)) # 使用 reduce
# 输出:24
解释:这里使用了 operator.mul
(乘法运算符)来逐步将列表中所有数字相乘。
3. 其他内置高阶函数:all
和 any
这两个函数本质上是简化逻辑判断的高阶函数。
all(iterable)
:如果迭代器中所有元素为真,返回 True;否则返回 False。any(iterable)
:如果迭代器中有任意一个元素为真,返回 True;否则返回 False。
示例:
# 使用 all 判断
print(all([1, True, 'non-empty'])) # True (所有元素为真)
print(all([1, 0, 'non-empty'])) # False (0 为假)
# 使用 any 判断
print(any([0, False, None, 'hello'])) # True (至少有一个元素为真)
print(any([0, False, None])) # False (所有元素为假)
补充例子:
# 检查一个列表是否全为正数
nums = [2, 4, 6, -1]
print(all(n > 0 for n in nums)) # False
print(any(n > 0 for n in nums)) # True
4. 匿名函数 lambda
在高阶函数中,我们经常希望创建临时的小函数,此时可以用 lambda
表达式:
- 格式:
lambda 参数: 返回值
例子:对一个列表按元素的平方排序:
nums = [1, -3, 2, -5, 4]
print(sorted(nums, key=lambda x: x**2))
# 输出:[1, 2, -3, 4, -5]
5. 高阶函数的关键点总结
- 高阶函数:接收函数作为参数或返回函数。
- 常见高阶函数:
map
、filter
、reduce
,但现代 Python 更推荐使用列表推导式、生成器与内置函数(如sum
)。 - 内置高阶函数:
sorted
、all
和any
。 - 匿名函数:
lambda
用于创建小型函数,方便高阶函数调用。
3、Anonymous Functions
上面提到了,这里更详细的讲解
1. 什么是匿名函数?
匿名函数是没有显式名称的函数,定义时使用 lambda
关键字,它主要用于一些简单的、一行完成需求的函数。
语法格式:
lambda 参数1, 参数2, ...: 表达式
- 参数部分:可以有一个或多个参数,类似于普通函数的参数。
- 表达式部分:lambda 函数的主体必须是一个单一的表达式,不能包含复杂的逻辑,如
while
、try
等语句。
注意:由于限制较多,
lambda
更适合简单场景,如果逻辑稍复杂,建议用def
定义普通函数。
2. 核心限制
- 不能包含复杂语句:如
if
、while
、try
等控制流语句。这是因为lambda
旨在保持小而简洁。 - 不能直接赋值给变量:例如
x = 10
的赋值语句在lambda
中会导致语法错误。 - 适配
:=
(赋值表达式):从 Python 3.8 起,支持使用:=
(海象操作符)在表达式中赋值。然而,代码一旦需要:=
,可能已经变得复杂,不适合使用lambda
。
3. 匿名函数的最佳实践: 高阶函数的参数
lambda
的经典应用场景是作为 高阶函数(如 sorted
,map
,filter
,reduce
等)的参数。
例子 1:以 sorted()
函数排序单词表为例
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
# 根据每个单词的“反向拼写”来排序
result = sorted(fruits, key=lambda word: word[::-1])
print(result)
# 输出:['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
解释:
key=lambda word: word[::-1]
的含义是,为每个单词生成基于其反转字符串的计算结果,然后按照这些计算结果排序。[::-1]
是切片操作,用于字符串反转。
4. 实际使用中的局限性
lambda
的使用不应复杂化:
如果 lambda
超出简单操作,变得难以阅读或维护,请用普通函数代替。遵照以下几点:
- 一点哲理:代码是写出来给人看的,顺带可以运行。如果其他人(或者未来的你)阅读代码时被复杂的
lambda
搞懵了,那就坚决不用。 - 实用步骤:根据 Fredrik Lundh 的建议,下面是重构复杂
lambda
的方法:- 写注释:解释这段
lambda
的功能。 - 思考名称:为它起一个合适的函数名。
- 用
def
重写它。 - 删除注释,因为名字和函数已经可以表达含义。
- 写注释:解释这段
例子 2:复杂 lambda
的重构
假设你看到下面这段让人费解的代码:
func = lambda x: (x**2 if x > 0 else -x) + 2*x
通过重构,可以优化为:
# 写注释:这个函数对正数平方后加 2 倍本身,对负数则取绝对值加 2 倍本身
def transform(x):
if x > 0:
return x**2 + 2*x
else:
return -x + 2*x
# 调用方式保持不变
func = transform
对比优劣:
- 原
lambda
的逻辑复杂且不易解读,容易产生误解和错误。 - 重构后的函数明确表达了逻辑,代码可维护性更高。
5. 为什么匿名函数是 “语法糖”?
定义差异:
lambda
是一个简化的语法糖,它帮助快速定义函数,不需要显式写def
关键字和函数名。- 本质上,
lambda
和def
创建的匿名函数和普通函数没有本质区别,生成的都是 函数对象。
例子 3:用 def
和 lambda
实现相同功能
# 使用 lambda
add_lambda = lambda x, y: x + y
# 使用 def
def add_def(x, y):
return x + y
# 它们本质相同
print(add_lambda(2, 3)) # 输出: 5
print(add_def(2, 3)) # 输出: 5
print(type(add_lambda)) # 输出: <class 'function'>
print(type(add_def)) # 输出: <class 'function'>
6. 补充例子:匿名函数与高阶函数结合
例子 4:使用匿名函数实现简单功能
-
和
map
配合:每个元素平方nums = [1, 2, 3, 4] squares = list(map(lambda x: x**2, nums)) print(squares) # 输出: [1, 4, 9, 16]
-
和
filter
配合:筛选偶数nums = [1, 2, 3, 4] evens = list(filter(lambda x: x % 2 == 0, nums)) print(evens) # 输出: [2, 4]
例子 5:替换复杂逻辑为普通函数
如果遇到既要筛选偶数又要平方的需求,避免嵌套 lambda
的复杂性:
nums = [1, 2, 3, 4]
# 用 lambda 嵌套(不推荐)
result = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)))
print(result) # 输出: [4, 16]
# 用普通函数重构
def is_even(x):
return x % 2 == 0
def square(x):
return x**2
result = list(map(square, filter(is_even, nums)))
print(result) # 输出: [4, 16]
总结
- 匿名函数是 简洁工具,适合简单逻辑,比如在排序或高阶函数中传递临时逻辑。
- 对于复杂逻辑,优先考虑用
def
定义普通函数。 - 重构准则:当
lambda
成为理解障碍时,用重构流程优化代码。 - 记住边界:
lambda
更适合小范围高效表达,不适合复杂方案。
4、The Nine Flavors of Callable Objects
什么是可调用对象?
一个对象是否可调用,可以通过内置函数 callable()
检测。如果返回值是 True
,那么该对象可以通过调用运算符 ()
使用。例如:
>>> callable(len) # 内置函数
True
>>> callable(str) # 类
True
>>> callable("Hello") # 字符串,不能调用
False
Python 中的 9 种可调用对象
以下是 Python 3.9 数据模型文档列出的九种可调用对象,按其特性和日常用途讲解。
1. 用户自定义函数(User-defined functions)
这些函数是使用 def
语句或 lambda
表达式定义的。
例子:
def add(x, y):
return x + y
lambda_func = lambda x: x * 2 # 使用 lambda 定义的函数
print(add(3, 5)) # 输出 8
print(lambda_func(4)) # 输出 8
易错点补充:
- 注意,
lambda
定义的函数是匿名的,不能指定函数名,但仍然是可调用对象。 - 即使没有显式的返回值,函数也是可调用的,例如:
def do_nothing(): pass print(callable(do_nothing)) # True
2. 内置函数(Built-in functions)
这些是 Python 提供的用 C 语言实现的内置函数,比如 len()
或 time.time()
。
例子:
print(len("Python")) # 输出 6
import time
print(time.time()) # 输出当前的时间戳
3. 内置方法(Built-in methods)
这是实现于 C 的方法,通常与类的内部操作相关,比如 dict.get()
。
例子:
my_dict = {'a': 1, 'b': 2}
print(my_dict.get('a')) # 输出 1
print(my_dict.get('c', 'not found')) # 输出 'not found'
区别:
- 内置函数是独立的,而内置方法属于某个对象,例如字典、列表等。
4. 方法(Methods)
定义在类中的函数,称为方法。它属于类的实例并可被调用。
例子:
class Greeter:
def greet(self, name):
return f"Hello, {name}!"
g = Greeter()
print(g.greet("Python")) # 输出 'Hello, Python!'
5. 类(Classes)
类本身也是可调用的,调用类时会运行其 __new__
方法来创建实例,然后运行 __init__
方法来初始化实例,最后返回实例。
例子:
class MyClass:
def __init__(self, value):
self.value = value
instance = MyClass(42) # 调用类,创建一个实例
print(instance.value) # 输出 42
进一步说明:
- 如果类重写了
__new__
方法,我们可以修改实例的创建过程。例如:class Singleton: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance a = Singleton() b = Singleton() print(a is b) # 输出 True,始终创建同一个实例
6. 类实例(Class Instances)
如果一个类定义了 __call__
方法,那么它的实例可以像函数一样调用。
例子:
class Counter:
def __init__(self, start=0):
self.count = start
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter()) # 输出 1
print(counter()) # 输出 2
易错点:
- 如果没有定义
__call__
方法,类实例是不可调用的。例如:class A: pass a = A() # print(a()) # 会报错:TypeError: 'A' object is not callable
7. 生成器函数(Generator Functions)
使用 yield
的函数被称为生成器函数。调用生成器函数会返回一个生成器对象,而不是直接执行函数体。
例子:
def countdown(n):
while n > 0:
yield n
n -= 1
gen = countdown(3) # 调用生成器函数,返回生成器对象
print(next(gen)) # 输出 3
print(next(gen)) # 输出 2
注意:
生成器对象是可迭代的,但调用生成器函数本身不会对数据产生直接影响。
8. 原生协程函数(Native Coroutine Functions)
使用 async def
定义的函数被称为原生协程函数,调用它们会返回协程对象。
例子(需要 Python 3.5+):
import asyncio
async def greet():
return "Hello, async!"
coroutine = greet()
print(coroutine) # 输出 <coroutine object greet at ...>
# 使用异步框架运行协程
result = asyncio.run(greet())
print(result) # 输出 'Hello, async!'
9. 异步生成器函数(Asynchronous Generator Functions)
在 async def
中使用 yield
的函数被称为异步生成器函数。调用它们会返回异步生成器对象,用于异步迭代。
例子(需要 Python 3.6+):
async def async_countdown(n):
while n > 0:
yield n
n -= 1
# 使用异步框架运行
async def main():
async for number in async_countdown(3):
print(number)
asyncio.run(main()) # 输出 3, 2, 1
可调用对象的检测
因为可调用对象形式多样,用 callable()
是最安全的检测方法。
例子:
print(callable(len)) # True
print(callable(str)) # True
print(callable(42)) # False
通过 callable()
,你可以快速判断对象是否可以使用 ()
运算符调用。
总结
类型 | 特性 | 示例 |
---|---|---|
用户自定义函数 | 用 def 或 lambda 定义 | def foo(): pass |
内置函数 | 由 Python 内部 C 语言实现 | len() , time.strftime() |
内置方法 | 与具体对象绑定的方法 | dict.get , list.append |
方法 | 类中的函数,属于实例 | obj.method() |
类 | 调用类会创建实例 | instance = MyClass() |
类实例 | 如果定义了 __call__ ,类实例可以被调用 | instance() |
生成器函数 | 使用 yield 的函数,返回生成器对象 | def gen(): yield 1 |
原生协程函数 | 用 async def 定义,返回协程对象 | async def func(): pass |
异步生成器函数 | 用 async def 定义且包含 yield ,返回异步生成器对象 | async def func(): yield 1 |
5、User-Defined Callable Types
1. 概念介绍:用户自定义可调用类型
在 Python 中,函数不仅仅是工具——它们本身是对象。而 Python 允许我们将任意对象变成“像函数一样”的可调用对象。这种特性是通过实现对象的 __call__
方法来实现的。
用户自定义的可调用类型就是那些通过实现 __call__
方法的类。这些对象可以像普通函数一样被调用(加括号 ()
形式),并且 Python 的 callable()
内置函数会将这些对象识别为“可调用”的。
2. 通过例子理解可调用类型:BingoCage 类
以下是实现一个自定义可调用类型的完整代码示例:
import random
class BingoCage:
def __init__(self, items):
# 接收任意可迭代对象,并将其转换为列表
# 防止直接操作输入的原始数据,从而避免副作用
self._items = list(items)
# 随机打乱 self._items 的顺序
random.shuffle(self._items)
def pick(self):
try:
# 从打乱的列表中弹出(移除并返回)一个随机项
return self._items.pop()
except IndexError:
# 如果 self._items 为空,抛出自定义的 LookupError 异常
raise LookupError('pick from empty BingoCage')
def __call__(self):
# 使对象可像函数一样被调用,相当于调用 self.pick()
return self.pick()
功能解析:
__init__
构造方法接受任何可迭代对象,创建一个局部拷贝并打乱顺序。这样,避免直接修改外部的输入数据。pick
方法从列表中随机取出一个元素(pop()
),并在列表空时抛出LookupError
异常。__call__
方法允许对象本身以函数形式调用,是通向“可调用对象”特性的桥梁。
3. BingoCage 的实例与调用演示
以下是关于 BingoCage
的使用示例:
>>> bingo = BingoCage(range(3)) # 初始化 bingo 对象,内部存储 [0, 1, 2] 的随机排列
>>> bingo.pick() # 手动调用 pick 方法取一个随机值
1
>>> bingo() # 通过 __call__ 方法直接调用对象
0
>>> callable(bingo) # 检查对象是否是可调用的
True
理解点:
- 调用
bingo.pick()
和bingo()
的作用是等价的。后者是实现了__call__
方法之后的“简写”。 callable()
返回True
,说明bingo
是一个满足“函数行为”的对象。
4. 可调用类型的实际用途
实现 __call__
的对象在 Python 中有许多用武之地,其中主要包括:
-
具有内部状态的函数对象:
- 可以通过
__call__
创建类似函数的对象,但它持有内部状态。例如,BingoCage
类通过其内部列表_items
保留了序列的状态。
- 可以通过
-
装饰器:
- 装饰器本质上需要是可调用对象,而实现
__call__
可以在装饰器内保存状态或者分步骤实现复杂的功能。
- 装饰器本质上需要是可调用对象,而实现
这里对为什么要在工程中使用__call__
的,可以去查看,比如pytorch里面就有使用等。
5. 对比:闭包与 __call__
Python 也可以通过闭包来实现具有内部状态的“函数”,这是一种函数式编程方法。两者的异同:
特点 | 使用类 + __call__ | 使用闭包 |
---|---|---|
状态管理 | 状态保存在类的属性中 | 状态保存在闭包的自由变量中 |
代码组织 | 适合复杂逻辑,可以分割成多个方法结构清晰 | 对简单逻辑适合,代码较为简洁 |
灵活性 | 可继承和复用,扩展时容易 | 无法扩展或继承功能,局限于函数定义中 |
6. 可能的易错点与深入补充
易错点 1:缺少 list(items)
可能导致的问题
- 如果在
__init__
中直接对用户传入的迭代器操作,没有拷贝将修改原始输入:
class WrongBingoCage:
def __init__(self, items):
self._items = items # 忘记拷贝
random.shuffle(self._items) # list 之外的可迭代对象可能报错!
正确方法:使用 list(items)
确保 _items
必定是 list
类型,避免潜在错误。
易错点 2:pop 的使用
- 在
pop()
操作时,如果列表为空会抛出IndexError
。需要提前检测或捕获异常。通过捕获并重新抛出LookupError
,设计更优用户体验。
7. 补充例子:统计调用次数的函数对象
为了帮助理解具有内部状态的可调用对象,再举一个简单示例:
class Counter:
def __init__(self):
self.count = 0 # 记录调用次数
def __call__(self, step=1):
self.count += step
return self.count
# 实例化 Counter 对象
counter = Counter()
print(counter()) # 输出:1
print(counter()) # 输出:2
print(counter(3)) # 输出:5
解析:
Counter
对象用__call__
实现了类似函数的行为,同时保留了调用次数作为内部状态。- 每次调用,
self.count
会根据传入参数累加,从而记录调用步骤。
8. 总结:学习要点
__call__
方法让对象变得像函数一样可调用,通过定义它可以自定义对象的调用行为。- 局部拷贝和数据保护:在类中,拷贝输入数据可以防止原始数据被修改,以避免意外副作用。
- 实用场景:
- 持有内部状态的函数对象,如统计数据、状态管理等;
- 设计装饰器。
- 掌握规范:为了更便于维护和扩展,始终保持代码结构清晰、功能职责单一。
这一工具是 Python 高级编程的重要基础,掌握它,为理解装饰器、闭包等概念奠定基础!
6、From Positional to Keyword-Only Parameters
一、函数参数的类型和灵活性
Python 函数支持以下几种参数形式:
- 位置参数 (
positional arguments
):按顺序传递到函数的参数。 - 关键字参数 (
keyword arguments
):显式地以键值对的方式传递到函数的参数。 - 可变位置参数 (
*args
):收集不固定数量的位置参数。 - 可变关键字参数 (
**kwargs
):收集不固定数量的关键字参数。 - 仅关键字参数 (
keyword-only arguments
):这些参数只能通过关键字方式传递。 - 仅位置参数 (
positional-only arguments
):这些参数只能通过位置方式传递。
二、关键字参数的处理机制
示例代码 1:tag
函数 —— 生成 HTML 标签
以下是一个使用了多种参数类型的函数 tag
,用来生成 HTML 标签。
def tag(name, *content, class_=None, **attrs):
"""生成一个或多个 HTML 标签"""
if class_ is not None:
attrs['class'] = class_ # 'class_' 避免与 Python 的关键字 class 冲突
# 生成属性字符串
attr_pairs = (f' {attr}="{value}"' for attr, value in sorted(attrs.items()))
attr_str = ''.join(attr_pairs) # 拼接所有属性为字符串
# 根据 content 是否存在,生成闭合标签或单标签
if content:
elements = (f'<{name}{attr_str}>{c}</{name}>' for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'
示例调用 1:tag
的使用方式
来看几个实际调用并分析其行为:
# 1. 生成一个单一的空标签
tag('br') # 输出:'<br />'
# 2. 生成带内容的标签
tag('p', 'hello') # 输出:'<p>hello</p>'
# 3. 生成多个带内容的重复标签
print(tag('p', 'hello', 'world'))
# 输出:
# <p>hello</p>
# <p>world</p>
# 4. 使用关键字参数指定属性
tag('p', 'hello', id=33) # 输出:'<p id="33">hello</p>'
# 5. 使用 class_ 参数作为关键字
print(tag('p', 'hello', 'world', class_='sidebar'))
# 输出:
# <p class="sidebar">hello</p>
# <p class="sidebar">world</p>
# 6. 参数为关键字参数时,不强制顺序
tag(content='testing', name='img') # 输出:'<img content="testing" />'
# 7. 使用 ** 解包字典传递参数
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'class': 'framed'}
tag(**my_tag)
# 输出:'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
函数行为总结:
*content
: 捕获所有多余的位置参数,作为一个元组。**attrs
: 捕获所有未在函数定义中显式声明的关键字参数,作为一个字典。class_
: 只能作为关键字参数传递,不允许位置方式传参。- 字典解包
**
:将字典中的键值对直接展开为关键字参数。
三、关键字参数深入讲解:仅关键字参数(keyword-only arguments
)
定义和特点:
- 仅关键字参数是在 Python 3 引入的一种参数,要求必须通过关键字的形式传递。
- 定义方式是在函数的形参列表中,用一个
*
来区分:*
后的参数即为关键字参数。
举例:关键字参数 class_
在 tag
函数中,class_
是关键字参数。调用时必须使用 class_=
的形式传递,而不能直接传递值。
def tag(*, class_=None, **attrs):
pass
tag(class_='test') # Correct
tag('test') # TypeError: tag() takes 0 positional arguments but 1 was given
如果不需要 *args
参数,但仍希望定义仅关键字参数
可以单独使用一个 *
:
def f(a, *, b):
return a, b
f(1, b=2) # 输出:(1, 2)
f(1, 2) # TypeError: f() takes 1 positional argument but 2 were given
注意:关键字参数可以是必需的(不提供默认值)。例如上例中,b
是必须通过关键字传递的,而 a
是位置参数。
四、位置参数深入讲解:仅位置参数(positional-only arguments
)
定义和特点:
- Python 3.8 引入了仅位置参数的支持(通过
/
指定)。 - 这种参数只能通过位置方式传递,不能以关键字形式调用。
举例:仅位置参数
以下是一个使用了仅位置参数的函数。
def divmod(a, b, /):
return (a // b, a % b)
divmod(10, 3) # 输出:(3, 1)
divmod(a=10, b=3) # TypeError: divmod() got some positional-only arguments
用法场景:
- 出于兼容性考虑——有些函数设计时不需要支持关键字参数,明确要求使用位置调用。
- 示例:修改
tag
函数使第一个参数name
为仅位置参数:def tag(name, /, *content, class_=None, **attrs): pass tag('p', 'hello', class_='main') # Correct tag(name='p') # TypeError: tag() got some positional-only arguments
五、总结:
Python 提供了优秀的参数灵活性,但不同类型参数的使用需要注意以下几点:
*args
和**kwargs
:分别捕获多余的位置参数和关键字参数。- 关键字参数:通过
*
可以限制某些参数必须通过关键字形式传递。 - 仅位置参数:通过
/
可以限制某些参数只允许通过位置传递。 - 优雅处理冲突:比如
class_
解决了与class
关键字的冲突问题。
通过实践这些特性,能够写出功能强大且灵活性高的函数,从而提升代码质量和可用性。
7、Packages for Functional Programming
总览
尽管Python的设计初衷并非为函数式编程而生,但其支持一等函数(first-class functions)和许多工具模块(如operator
和functools
),使得函数式编程风格也可以在Python中大显身手。我们将逐步讲解如何使用operator
模块替换冗长的lambda
表达式,以及如何用functools.partial
冻结参数变得更高效。
1. operator
模块
概述
operator
模块是一组函数的集合,它用来替代直接使用lambda
定义一些功能简单、但常见的匿名函数,例如元素求和、乘积计算、按索引取值等操作。
优点:
- 代码可读性高,减少对
lambda
表达式的依赖。 - 提供了大量简单函数,比如数学运算、列表操作符重载等。
示例1:使用operator.mul
替代lambda
进行阶乘计算
原始方式 — 使用lambda
from functools import reduce
def factorial(n):
return reduce(lambda a, b: a * b, range(1, n + 1))
print(factorial(5)) # 输出: 120
改进方式 — 使用operator.mul
operator.mul
对两个数执行乘法操作:
from functools import reduce
from operator import mul
def factorial(n):
return reduce(mul, range(1, n + 1))
print(factorial(5)) # 输出: 120
相比lambda a, b: a * b
,mul
更简洁、更具语义化。仍然是累积乘法,代码却变得干净直观。
示例2:itemgetter
按索引获取值或按多重键排序
功能及易错点
itemgetter
生成一个函数,该函数用于从序列(如列表、元组)或字典中根据索引或键提取值。这避免了冗长lambda
代码,并且不仅支持列表,还支持字典及__getitem__
协议对象。
使用itemgetter
排序:
from operator import itemgetter
metro_data = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
# 按国家代码 (索引1) 排序
for city in sorted(metro_data, key=itemgetter(1)):
print(city)
输出:
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
进一步示例:返回多个索引值
如果传递多个索引,itemgetter
返回对应值的tuple
:
cc_name = itemgetter(1, 0) # 按索引1和索引0分别提取字段
for city in metro_data:
print(cc_name(city))
输出:
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'São Paulo')
注意点:
itemgetter
不仅支持列表/元组,还支持字典及其他实现了__getitem__
的对象。
示例3:attrgetter
按属性提取值或多重嵌套
区别于itemgetter
按索引操作,attrgetter
通过属性名提取对象属性值,尤其适合命名元组或对象实例。
示例:城市按纬度排序
from collections import namedtuple
from operator import attrgetter
LatLon = namedtuple('LatLon', 'lat lon')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
# 定义嵌套的命名元组
metro_areas = [
Metropolis('Tokyo', 'JP', 36.933, LatLon(35.689722, 139.691667)),
Metropolis('Delhi NCR', 'IN', 21.935, LatLon(28.613889, 77.208889)),
Metropolis('Mexico City', 'MX', 20.142, LatLon(19.433333, -99.133333)),
Metropolis('New York-Newark', 'US', 20.104, LatLon(40.808611, -74.020386)),
Metropolis('São Paulo', 'BR', 19.649, LatLon(-23.547778, -46.635833))
]
# 使用attrgetter提取纬度并排序
for city in sorted(metro_areas, key=attrgetter('coord.lat')):
print(attrgetter('name', 'coord.lat')(city)) # 同时提取城市名称和纬度
输出:
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
注意:
- 当属性是嵌套结构时,
attrgetter
支持用.
导航获取内部属性,如coord.lat
。 - 与
itemgetter
一样,也支持返回tuple
。
示例4:methodcaller
绑定方法并调用
methodcaller
通过方法名创建函数(支持绑定额外参数)。适合传递给高阶函数。
示例:
from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper') # 绑定str.upper方法
print(upcase(s)) # 输出: 'THE TIME HAS COME'
hyphenate = methodcaller('replace', ' ', '-') # 将空格替换为-
print(hyphenate(s)) # 输出: 'The-time-has-come'
2. functools.partial
— 冻结部分参数
概述
通过partial
,我们可以创建一个新的函数,将已有函数的部分参数提前绑定。这在需要减少回调函数参数数量时非常实用。
示例1:冻结参数
假定我们有一个乘法函数mul
,需要一个专门的"三倍数"函数:
from operator import mul
from functools import partial
# 创建一个新的函数 triple,绑定第一个参数为3
triple = partial(mul, 3)
print(triple(7)) # 输出: 21
# 将 triple 应用于一组数字
print(list(map(triple, range(1, 10)))) # 输出: [3, 6, 9, 12, 15, 18, 21, 24, 27]
示例2:绑定Unicode标准化处理
Unicode文本有时会有不同的表示(如café
和cafe\u0301
),为了保持一致,可以绑定unicodedata.normalize
的NFC
参数:
import unicodedata
from functools import partial
nfc = partial(unicodedata.normalize, 'NFC')
s1 = 'café'
s2 = 'cafe\u0301'
print(nfc(s1) == nfc(s2)) # 输出: True
示例3:冻结多个参数
例子中冻结了第一个位置参数和某些关键字参数:
from functools import partial
def tag(name, **attributes):
attrs = ' '.join(f'{key}="{value}"' for key, value in attributes.items())
return f'<{name} {attrs} />'
# 冻结参数 name='img' 和 class_='pic-frame'
picture = partial(tag, 'img', class_='pic-frame')
print(picture(src='image.jpg')) # 输出: '<img class_="pic-frame" src="image.jpg" />'
总结
operator
模块:
- 使用
operator
中的工具,使得代码更简洁和语义化。 - 推荐用
itemgetter
/attrgetter
替代lambda
提取值;推荐methodcaller
结合方法使用。
functools.partial
:
- 简化使用:冻结函数的一部分参数。
- 常用于API适配、回调函数优化,以及特定功能封装。
8、Chapter Summary
本章的目标是探究Python中函数的一等对象特性。主要观点是,你可以将函数赋值给变量、传递给其他函数、存储在数据结构中,还能访问函数的属性,这使得框架和工具能够利用这些信息。
高阶函数是函数式编程的主要内容,在Python中也很常见。内置函数sorted
、min
、max
以及functools.partial
都是Python中常用的高阶函数示例。由于列表推导式(以及类似生成器表达式这样的结构)的出现,还有像sum
、all
、any
这些内置的聚合函数,map
、filter
和reduce
的使用不像以前那么频繁了。
自Python 3.6起,可调用对象有九种不同类型,从用lambda
创建的简单函数,到实现了__call__
方法的类实例。生成器和协程也是可调用的,尽管它们的行为与其他可调用对象有很大不同。所有可调用对象都可以用内置的callable()
函数来检测。可调用对象在声明形式参数时提供了丰富的语法,包括仅限关键字参数、仅限位置参数和注解。
最后,我们介绍了operator
模块中的一些函数以及functools.partial
,它们通过减少对使用受限的lambda
语法的需求,为函数式编程提供了便利。