CHAPTER 7 Functions as First-Class Objects

开启了新一篇章,之前的章节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): 用于动态分配内存,如创建对象、存储数据(例如 listdict 的元素)。
  • 栈(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 (动态变化)        │              │
└────────────────────┴──────────────────────────────────┴──────────────┘

解释:

  1. 栈:

    • 存储了局部变量 rit
    • r 指向了位于堆中的 range 对象。
    • it 是从 r 派生的迭代器,其中存储了 当前状态 current = 1(当前应该生成的值)。
  2. 堆:

    • 堆中有一个 range 对象,包含了生成规则(start=1, stop=5, step=1)。
    • 一个迭代器对象派生自 range,它保存当前迭代的上下文信息(例如 current=3)。
  3. 常量区:

    • 存储了 Python 程序中定义的常量数据(比如 15)。

内存动态更新过程(图解)

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)
    1. 返回当前值:current=1
    2. 更新上下文状态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] |               │
└──────────────┴────────────────────┴──────────────┘

文件迭代的内存更新过程:

  1. 初始阶段:

    • 文件缓冲区为空,文件指针指向文件开头位置。
    • 内存中仅仅保存了文件对象。
  2. 第一次调用:

    • 缓冲区加载了一部分文件内容(少量数据)。
    • 文件指针更新到了下一行的开头位置。
    • 数据消耗一行并输出结果,该行即被丢弃(不再占用内存)。
栈:
  f -> 文件对象
堆:
  文件对象: 缓冲区只存储 “line1” ,文件指针位于 line2 的开头
  当前行: "line1"
  1. 逐渐迭代整个文件:
    • 缓冲区内容按需加载。
    • 逐行处理,丢弃之前已经使用的行。
    • 文件指针逐渐扫描整个文件,提高效率,节省内存。

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. 总结:迭代器与内存管理之关键点

  1. 按需加载,动态生成:
    迭代器只存储“当前状态”(上下文),而非整个数据集合,节省内存。

  2. 堆与栈的分工:

    • 栈中存储局部变量,例如迭代器对象。
    • 堆中存储核心上下文信息,例如 current、缓冲区等。
  3. 常量区存储基础规则:

    • 不会存储实际计算结果,仅保存生成规则。
  4. 大数据/无限数据流支持:

    • 数据一旦被消费,就可以丢弃。
    • 适合高效处理大文件、流数据或动态生成的序列。

1、Treating a Function Like an Object

一等函数的定义

一个语言中的函数被称为“一等函数”时,它需满足以下条件:

  1. 函数可以作为变量赋值。
  2. 函数可以作为参数传递给另一个函数。
  3. 函数可以作为函数的返回值。

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'>                                      # 确认函数的类型

关键点:

  1. 函数是对象:

    • 函数 factorialfunction 类的实例,可以被操作,例如调用 (factorial(42)) 或访问属性(如 factorial.__doc__)。
    • 使用 type(factorial) 可以看到它的类型是 <class 'function'>
  2. 函数的 __doc__ 属性:

    • 存储了函数的文档字符串(docstring),在帮助文档中生成 help() 的输出内容。例如,输入 help(factorial) 时,会显示 returns n!

第二部分:函数的“一等性质”

例子分析 7-2:

  1. 将函数赋值给变量:

    >>> fact = factorial
    >>> fact
    <function factorial at 0x...>
    >>> fact(5)
    120
    
    • factorial 赋值给另一个变量 fact。此时,factfactorial 指向同一个函数对象。
    • 我们可以像调用 factorial 一样调用 fact,返回结果 120(即 5!)。
  2. 将函数作为参数传递:

    >>> 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 接收两个参数:
      1. 一个函数(factorial)。
      2. 一个可迭代对象(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

注意点总结

  1. 赋值不等于复制函数:

    • 当我们将函数赋值给另一个变量时,并不是重新创建了该函数,而是创建了对原函数对象的引用。
    • factfactorial 指向的是同一个对象。
  2. map 的返回值:

    • map 返回的是一个映射对象,不是直接的列表。需要使用 list() 来获取最终结果。
  3. 错误用法:

    • 不要在函数名后面加 () 传递。例如 map(factorial(), range(11)) 是错误的,因为 factorial() 会被立即执行而不是作为函数传递。

总结

将函数视为对象是 Python 函数式编程的基础。通过掌握函数的以下特性:

  1. 变量赋值:函数可以被赋值给变量。
  2. 参数传递:函数可以作为参数传递给其他函数。
  3. 返回值:函数可以作为其他函数的返回值。

我们能够编写更灵活、更强大的代码,并为后续深入了解高阶函数和函数式编程奠定基础。

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']

补充解释

  1. word[::-1] 是字符串切片,用于反转字符串。
  2. 原始单词列表未改变,仅通过 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 提供了一些经典高阶函数:mapfilterreduce
但随着 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. 其他内置高阶函数:allany

这两个函数本质上是简化逻辑判断的高阶函数

  • 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. 高阶函数的关键点总结

  1. 高阶函数:接收函数作为参数或返回函数。
  2. 常见高阶函数mapfilterreduce,但现代 Python 更推荐使用列表推导式、生成器与内置函数(如 sum)。
  3. 内置高阶函数sortedallany
  4. 匿名函数lambda 用于创建小型函数,方便高阶函数调用。

3、Anonymous Functions

上面提到了,这里更详细的讲解

1. 什么是匿名函数?

匿名函数是没有显式名称的函数,定义时使用 lambda 关键字,它主要用于一些简单的、一行完成需求的函数。

语法格式:

lambda 参数1, 参数2, ...: 表达式
  • 参数部分:可以有一个或多个参数,类似于普通函数的参数。
  • 表达式部分:lambda 函数的主体必须是一个单一的表达式,不能包含复杂的逻辑,如 whiletry 等语句。

注意:由于限制较多,lambda 更适合简单场景,如果逻辑稍复杂,建议用 def 定义普通函数。


2. 核心限制

  • 不能包含复杂语句:如 ifwhiletry 等控制流语句。这是因为 lambda 旨在保持小而简洁。
  • 不能直接赋值给变量:例如 x = 10 的赋值语句在 lambda 中会导致语法错误。
  • 适配 :=(赋值表达式):从 Python 3.8 起,支持使用 :=(海象操作符)在表达式中赋值。然而,代码一旦需要 :=,可能已经变得复杂,不适合使用 lambda

3. 匿名函数的最佳实践: 高阶函数的参数

lambda 的经典应用场景是作为 高阶函数(如 sortedmapfilterreduce 等)的参数。

例子 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 超出简单操作,变得难以阅读或维护,请用普通函数代替。遵照以下几点:

  1. 一点哲理:代码是写出来给人看的,顺带可以运行。如果其他人(或者未来的你)阅读代码时被复杂的 lambda 搞懵了,那就坚决不用。
  2. 实用步骤:根据 Fredrik Lundh 的建议,下面是重构复杂 lambda 的方法:
    1. 写注释:解释这段 lambda 的功能。
    2. 思考名称:为它起一个合适的函数名。
    3. def 重写它。
    4. 删除注释,因为名字和函数已经可以表达含义。

例子 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 关键字和函数名。
  • 本质上,lambdadef 创建的匿名函数和普通函数没有本质区别,生成的都是 函数对象

例子 3:用 deflambda 实现相同功能

# 使用 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:使用匿名函数实现简单功能

  1. map 配合:每个元素平方

    nums = [1, 2, 3, 4]
    squares = list(map(lambda x: x**2, nums))
    print(squares)  # 输出: [1, 4, 9, 16]
    
  2. 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]

总结

  1. 匿名函数是 简洁工具,适合简单逻辑,比如在排序或高阶函数中传递临时逻辑。
  2. 对于复杂逻辑,优先考虑用 def 定义普通函数。
  3. 重构准则:当 lambda 成为理解障碍时,用重构流程优化代码。
  4. 记住边界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(),你可以快速判断对象是否可以使用 () 运算符调用。


总结

类型特性示例
用户自定义函数deflambda 定义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 中有许多用武之地,其中主要包括:

  1. 具有内部状态的函数对象:

    • 可以通过 __call__ 创建类似函数的对象,但它持有内部状态。例如,BingoCage 类通过其内部列表 _items 保留了序列的状态。
  2. 装饰器:

    • 装饰器本质上需要是可调用对象,而实现 __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. 总结:学习要点

  1. __call__ 方法让对象变得像函数一样可调用,通过定义它可以自定义对象的调用行为。
  2. 局部拷贝和数据保护:在类中,拷贝输入数据可以防止原始数据被修改,以避免意外副作用。
  3. 实用场景
    • 持有内部状态的函数对象,如统计数据、状态管理等;
    • 设计装饰器。
  4. 掌握规范:为了更便于维护和扩展,始终保持代码结构清晰、功能职责单一。

这一工具是 Python 高级编程的重要基础,掌握它,为理解装饰器、闭包等概念奠定基础!

6、From Positional to Keyword-Only Parameters

一、函数参数的类型和灵活性

Python 函数支持以下几种参数形式:

  1. 位置参数 (positional arguments):按顺序传递到函数的参数。
  2. 关键字参数 (keyword arguments):显式地以键值对的方式传递到函数的参数。
  3. 可变位置参数 (*args):收集不固定数量的位置参数。
  4. 可变关键字参数 (**kwargs):收集不固定数量的关键字参数。
  5. 仅关键字参数 (keyword-only arguments):这些参数只能通过关键字方式传递。
  6. 仅位置参数 (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" />'
函数行为总结:
  1. *content: 捕获所有多余的位置参数,作为一个元组。
  2. **attrs: 捕获所有未在函数定义中显式声明的关键字参数,作为一个字典。
  3. class_: 只能作为关键字参数传递,不允许位置方式传参。
  4. 字典解包 **:将字典中的键值对直接展开为关键字参数。

三、关键字参数深入讲解:仅关键字参数(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 提供了优秀的参数灵活性,但不同类型参数的使用需要注意以下几点:

  1. *args**kwargs:分别捕获多余的位置参数和关键字参数。
  2. 关键字参数:通过 * 可以限制某些参数必须通过关键字形式传递。
  3. 仅位置参数:通过 / 可以限制某些参数只允许通过位置传递。
  4. 优雅处理冲突:比如 class_ 解决了与 class 关键字的冲突问题。

通过实践这些特性,能够写出功能强大且灵活性高的函数,从而提升代码质量和可用性。

7、Packages for Functional Programming

总览

尽管Python的设计初衷并非为函数式编程而生,但其支持一等函数(first-class functions)和许多工具模块(如operatorfunctools),使得函数式编程风格也可以在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 * bmul更简洁、更具语义化。仍然是累积乘法,代码却变得干净直观。


示例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.normalizeNFC参数:

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中也很常见。内置函数sortedminmax 以及functools.partial都是Python中常用的高阶函数示例。由于列表推导式(以及类似生成器表达式这样的结构)的出现,还有像sumallany这些内置的聚合函数,mapfilterreduce的使用不像以前那么频繁了。

自Python 3.6起,可调用对象有九种不同类型,从用lambda创建的简单函数,到实现了__call__方法的类实例。生成器和协程也是可调用的,尽管它们的行为与其他可调用对象有很大不同。所有可调用对象都可以用内置的callable()函数来检测。可调用对象在声明形式参数时提供了丰富的语法,包括仅限关键字参数、仅限位置参数和注解。

最后,我们介绍了operator模块中的一些函数以及functools.partial,它们通过减少对使用受限的lambda语法的需求,为函数式编程提供了便利。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值