《流畅的Python》 读书笔记 第5章 一等函数 20231025
1|0第5章 一等函数
第四章相对偏僻,但时间上一样要花我很久,就先跳过了,回头再补。而这个第5章节是非常重要的。只是最近工作有点忙,我读的越来越慢了~继续坚持吧。
在 Python 中,所有函数都是一等对象,整数、字符串和字典都是一等对象(注:first-class object)
要成为一等对象,需要满足
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
2|05.1 把函数视作对象
函数对象本身是 function 类的实例
__doc__
是函数对象众多属性中的一个
从上面的例子,你看到了2个特点
- 能赋值给变量
- 能作为参数传给函数
有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数
3|05.2 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)
在函数式编程范式中,最为人熟知的高阶函数有 map、filter、reduce
内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序
示例 5-4
3|1map、filter和reduce的现代替代品
列表推导或生成器表达式具有 map 和 filter 两个函数的功能,map 和 filter 还是内置函数
➊ 构建 0! 到 5! 的一个阶乘列表。
➋ 使用列表推导执行相同的操作。
➌ 使用 map 和 filter 计算直到 5! 的奇数阶乘列表。
➍ 使用列表推导做相同的工作,换掉 map 和 filter,并避免了使用 lambda 表达式
map 和 filter 返回生成器(一种迭代器),因此现在它们的直接替代品是生成器表达式
在Python3.9中我看到是一个map、filter对象,当然它也是可以迭代的
reduce在 Python 3 中放到 functools 模块里了。这个函数最常用于求和,自 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值
all 和 any 也是内置的归约函数。
all(iterable)
如果 iterable 的每个元素都是真值,返回 True;all([]) 返回 True。
any(iterable)
只要 iterable 中有元素是真值,就返回 True;any([]) 返回 False
4|05.3 匿名函数
为了使用高阶函数,有时创建一次性的小型函数更便利。这便是匿名函数存在的原因
示例 5-7
改写自5-4
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。
由于句法上的限制,非平凡的 lambda 表达式要么难以阅读,要么无法写出。
如果使用 lambda 表达式导致一段代码难以理解,Fredrik Lundh 建议像下面这样重构。
(1) 编写注释,说明 lambda 表达式的作用。
(2) 研究一会儿注释,并找出一个名称来概括注释。
(3) 把 lambda 表达式转换成 def 语句,使用那个名称来定义函数。
(4) 删除注释。
这几步摘自“Functional Programming HOWTO”(https://docs.python.org/3/howto/functional.
html),这是一篇必读文章
5|05.4 可调用对象
可调用对象 | 说明 |
---|---|
用户定义的函数 | 使用 def 语句或 lambda 表达式创建。 |
内置函数 | 使用 C 语言(CPython)实现的函数,如 len 或 time.strftime |
内置方法 | 使用 C 语言实现的方法,如 dict.get。 |
方法 | 在类的定义体中定义的函数 |
类 | 调用类时会运行类的 __new__ 方法创建一个实例,然后运行__init__ 方法,初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用 函数。(通常,调用类会创建那个类的实例,不过覆盖 __new__ 方法的话,也可能出现其他行为。 |
类的实例 | 如果类定义了 __call__ 方法,那么它的实例可以作为函数调用。 |
生成器函数 | 使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象。 |
Native coroutine functions本地协程函数 | Functions or methods defined with async def . When called, they return acoroutine object. Added in Python 3.5. |
synchronous generator functions异步生成器函数 | Functions or methods defined with async def that have yield in their body.When called, they return an asynchronous generator for use with async for. Added in Python 3.6 |
前7个是比较好理解的,最后两个是第二版加进来的,第一个是协程;
下面程序运行完毕后,时间就2s
与之相对的,下面这个代码就要执行3s
第二个是异步生成器
判断对象能否调用,最安全的方法是使用内置的 callable() 函数
6|05.5 用户定义的可调用类型
任何 Python 对象都可以表现得像函数。为此,只需实现实例方法
__call__
。
示例
➊ __init__
接受任何可迭代对象;在本地构建一个副本,防止列表参数的意外副作用。
➋ shuffle 定能完成工作,因为 self._items
是列表。
➌ 起主要作用的方法。
➍ 如果 self._items
为空,抛出异常,并设定错误消息。
➎ bingo.pick() 的快捷方式是 bingo()。
实现
__call__
方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间“记住”某些事 [ 例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ]。
创建保有内部状态的函数,还有一种截然不同的方式——使用闭包
装饰器不是可以是类吗?
看了下英文的第二版,是这么描述的:Decorators must be callable
,也不知道是改进了,还是中译错了~
7|05.6 函数内省
大多数属性是 Python 对象共有的
函数使用
__dict__
属性存储赋予它的用户属性,这相当于一种基本形式的注解
Django 框架为函数赋予属性,但不是很常见的做法
示例 5-9 列出常规对象没有而函数有的属性
名称 | 类型 | 说明 |
---|---|---|
__annotations__ |
dict | 参数和返回值的注解 |
__call__ |
method-wrapper | 实现()运算符:即可调用对象协议 |
__closure__ |
tuple | 函数闭包,即自由变量的绑定(通常是None) |
__code__ |
code | 编译成字节码的函数元数据和函数定义体 |
__defaults__ |
tuple | 形式参数的默认值 |
__get__ |
method-wrapper | 实现只读描述符协议 |
__globals__ |
dict | 函数所在模块中的全局变量 |
__kwdefaults__ |
dict | 仅限关键字形式参数的默认值 |
__name__ |
str | 函数名称 |
__qualname__ |
str | 函数的限定名称,如 Random.choice( 参 阅 PEP 3155,https://wwW.Python.org/dev/peps/pep-3155/) |
8|05.7 从定位参数到仅限关键字参数
关键字参数(keyword-only argument)
调用函数时使用 * 和 **
展开
可迭代对象,映射到单个参数
示例 5-10 tag 函数用于生成 HTML 标签
使用名为 cls 的关键字参数传入“class”属性,这是一种变通方法,因为“class”是 Python 的关键字
➊ 传入单个定位参数,生成一个指定名称的空标签。
➋ 第一个参数后面的任意个参数会被 *content 捕获,存入一个元组。
➌ tag 函数签名中没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。
➍ cls 参数只能作为关键字参数传入。
➎ 调用 tag 函数时,即便第一个定位参数也能作为关键字参数传入。
➏ 在 my_tag 前面加上**
,字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上,余下的则被 **attrs
捕获
输出
cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。
定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。
如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个 *
示例代码
报错
第二版中代码是这么写的
就几个变化
- 参数从cls编程了class_
- attr_str没任何处理,如果不传attr,那你得到的attr_str也是''
- 之前用%s这样的方式,现在用的是f-string
8|15.7.1 仅限位置参数 Positional-Only Parameters
这是第二版才有的内容
自Python 3.8版本以后,用户自定义的函数签名可以指定位置参数(positional-only parameters)
可以参考官网:https://docs.python.org/zh-cn/3.8/whatsnew/3.8.html#positional-only-parameters
也可以参考: https://peps.python.org/pep-0570/
最典型的就是divmod
意思就很明显了,/
前面的参数必须是位置参数,其后的不管
类似的*
后面的必须是关键字参数
9|05.8 获取关于参数的信息
这个章节,新版去掉了,放到了这里:https://www.fluentpython.com/extra/function-introspection/
示例 5-12 Bobo 知道 hello 需要 person 参数,并且从 HTTP 请求中获取它
要安装bobo
然后bobo -f demo.py
就可以curl了
这个时候console
跟当代的web框架:flask、Django的输出类似
这个框架是怎么做到知道你要传一个person参数的呢?
bobo.query 装饰器把一个普通的函数(如 hello)与框架的请求处理机制集成起来了
Bobo 会内省 hello 函数,发现它需要一个名为 person 的参数,然后从请求中获取那个名称对应的参数,将其传给
hello 函数,因此程序员根本不用触碰请求对象
怎么做到的呢?
Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?
函数对象有个__defaults__
属性,它的值是一个元组,里面保存着定位参数和关键字参
数的默认值。仅限关键字参数的默认值在__kwdefaults__
属性中。然而,参数的名称在
__code__
属性中,它的值是一个 code 对象引用,自身也有很多属性
__code__的属性非常多
参考: https://docs.python.org/zh-cn/3.9/library/inspect.html?highlight=co_name
参数 | 解释 |
---|---|
co_argcount | 参数数量(不包括仅关键字参数、* 或 ** 参数) |
co_code | 原始编译字节码的字符串 |
co_cellvars | 单元变量名称的元组(通过包含作用域引用) |
co_consts | 字节码中使用的常量元组 |
co_filename | 创建此代码对象的文件的名称 |
co_firstlineno | 第一行在Python源码的行号 |
co_flags | CO_* 标志的位图,详见 此处 |
co_lnotab | 编码的行号到字节码索引的映射 |
co_freevars | 自由变量的名字组成的元组(通过函数闭包引用) |
co_posonlyargcount | 仅限位置参数的数量 |
co_kwonlyargcount | 仅限关键字参数的数量(不包括 ** 参数) |
co_name | 定义此代码对象的名称 |
co_names | 局部变量名称的元组 |
co_nlocals | 局部变量的数量 |
co_stacksize | 需要虚拟机堆栈空间 |
co_varnames | 参数名和局部变量的元组 |
关于函数参数的个数
这种做法并不是最便利的
参数名称在
__code__.co_varnames
中,不过里面还有函数定义体中创建的局部变量。因此,参数名称是前 N 个字符串,N 的值由__code__.co_argcount
确定。顺便说一下,这里不包含前缀为 * 或 ** 的变长参数。参数的
默认值只能通过它们在__defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来
书中还提供了一个例子,你可以参考
更好的方式——使用 inspect 模块
示例 5-17 提取函数的签名
inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来
各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty 值表示没有默认值
但要注意=None是默认值,不是_empty
kind的取值可能
inspect.Parameter 对象还有一个 annotation(注解)属性,它的值通常是 inspect._empty,但是可能包含 Python 3 新的注解句法提供的函数签名元数据
inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数
输出
10|05.9 函数注解
这个章节在第二版中也挪到了typing部分中去
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据
函数声明中的各个参数可以在 : 之后增加注解表达式。
如果参数有默认值,注解放在参数名和 = 号之间。
如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。那个表达式可以是任何类型。
注解中最常用的类型是类(如 str 或 int)和字符串(如'int > 0')
Python 对注解所做的唯一的事情是,把它们存储在函数的
__annotations__
属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用
示例 5-20 从函数签名中提取注解
函数注解的最大影响或许不是让 Bobo 等框架自动设置,而是为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息
11|05.10 支持函数式编程的包
operator 和functools 等包的支持,函数式编程风格也可以信手拈来
11|15.10.1 operator模块
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。我们可以使用 reduce 函数,但是需要一个函数计算序列中两个元素之积
示例 5-21 使用 reduce 函数和一个匿名函数计算阶乘
示例 5-22 使用 reduce 和 operator.mul 函数计算阶乘
operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式
itemgetter 和 attrgetter 其实会自行构建函数
示例
再看个示例,结合sorted进行排序
书里的示例 5-23 演示使用 itemgetter 排序一个元组列表
根据metro_data中的子元素的第2个元素进行排序
如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组
itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现
__getitem__
方法的类
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组
此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性
示例 5-24 定义一个 namedtuple,名为 metro_data
➊ 使用 namedtuple 定义 LatLong。
➋ 再定义 Metropolis。
➌ 使用 Metropolis 实例构建 metro_areas 列表;注意,我们使用嵌套的元组拆包提取(lat, long),然后使用它们构建 LatLong,作为 Metropolis 的 coord 属性。
➍ 深入 metro_areas[0],获取它的纬度。
➎ 定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性。
➏ 再次使用 attrgetter,按照纬度排序城市列表。
➐ 使用标号➎中定义的 attrgetter,只显示城市名和纬度。
输出
operator 模块中定义的部分函数
methodcaller。它的作用与 attrgetter和 itemgetter 类似,它会自行创建函数。methodcaller 创建的函数会在对象上调用参数指定的方法
示例 5-25 methodcaller 使用示例:第二个测试展示绑定额外参数的方式
第一个测试并不推荐,纯粹的替换
第二个测试就略有意义,它绑定
或称之为冻结
了参数
等价于s.replace(' ','-')
11|25.10.2 使用functools.partial冻结参数
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定
使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少
示例 5-26 使用 partial 把一个两参数函数改编成需要单参数的可调用对象
➊ 使用 mul 创建 triple 函数,把第一个定位参数定为 3。
➋ 测试 triple 函数。
➌ 在 map 中使用 triple;在这个示例中不能使用 mul
实际上,mul这个函数必须要传2个参数:乘数和被乘数
当你做了triple = partial(mul, 3)
后,就固定了一个乘数
书中还提供了多个例子
示例 5-27 使用 partial 构建一个便利的 Unicode 规范化函数
示例 5-28 把 partial 应用到示例 5-10 中定义的 tag 函数上
➊ 从示例 5-10 中导入 tag 函数,查看它的 ID。
➋ 使用 tag 创建 picture 函数,把第一个定位参数固定为 'img',把 cls 关键字参数固定为 'pic-frame'。
➌ picture 的行为符合预期
➍ partial() 返回一个 functools.partial 对象。
➎ functools.partial 对象提供了访问原函数和固定参数的属性
functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算
12|05.11 本章小结
我们可以把函数赋值给变量、传给其他函数、存储在数据结构中,以及访问函数的属性,供框架和一些工具使用。
高阶函数是函数式编程的重要组成部分,即使现在不像以前那样经常使用 map、filter 和 reduce 函数了,但是还有列表推导(以及类似的结构,如生成器表达式)以及 sum、all 和 any 等内置的归约函数。
Python 中常用的高阶函数有内置函数 sorted、min、max 和 functools.partial。
Python 有 9
种可调用对象,从 lambda 表达式创建的简单函数到实现 __call__
方法的类实例。这些可调用对象都能通过内置的 callable() 函数检测。每一种可调用对象都支持使用相同的丰富句法声明形式参数,包括仅限关键字参数和注解——二者都是 Python 3 引入的新特性。
Python 函数及其注解有丰富的属性,在 inspect 模块的帮助下,可以读取它们。例如,Signature.bind 方法使用灵活的规则把实参绑定到形参上,这与 Python 使用的规则一样。
最后,本章介绍了 operator 模块中的一些函数,以及 functools.partial 函数,有了这些函数,函数式编程就不太需要功能有限的 lambda 表达式
13|05.12 延伸阅读
素材 | URL | 相关信息 |
---|---|---|
Python Cookbook(第 3 版)中文版》 | 第 7 章是对本书的 本章和第 7 章很好的补充 |
|
Python 语言参考手册中的“3.2. The standard type hierarchy” | https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy | 对 7 种可调用类型和其他所有内置类型做了介绍 |
PEP 3102—Keyword-Only Arguments | https://www.python.org/dev/peps/pep-3102 | |
PEP 3107—Function Annotations | www.python.org/dev/peps/pep-3107 | |
What are good uses for Python3’s ‘Function Annotations’ | http://stackoverflow.com/questions/3038033/what-are-good-uses-for-python3s-function-annotations | |
What good are Python function annotations? | http://stackoverflow.com/questions/13784713/what-good-are-python-function-annotations | |
PEP 362—Function Signature Object | https://www.python.org/dev/peps/pep-0362 | |
Python Functional Programming HOWTO | http://docs.python.org/3/howto/functional.html | |
fn.py | https://github.com/kachayev/fn.py | 是为 Python 2 和 Python 3 提供函数式编程支持的包;这个包提供的 @recur.tco 装饰器为 Python 中的无限递归实现了尾调用优化 |
Python: Why is functools.partial necessary? | http://stackoverflow.com/questions/3252228/python-why-is-functools-partial-necessary | |
Bobo | http://bobo.readthedocs.io/en/latest/ | 面向对象的 Web 框架 |
map、filter 和 reduce 的最初目的是为 Python 增加 lambda 表达式
lambda、map、filter 和 reduce 首次出现在 Lisp 中,这是最早的一门函数式语言
在任何一门语言中,匿名函数都有一个严重的缺点:没有名称。函数有名称,栈跟踪更易于阅读
__EOF__

本文链接:https://www.cnblogs.com/wuxianfeng023/p/17787439.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET10 - 预览版1新功能体验(一)