《流畅的Python》 读书笔记 第5章 一等函数 20231025
第5章 一等函数
第四章相对偏僻,但时间上一样要花我很久,就先跳过了,回头再补。而这个第5章节是非常重要的。只是最近工作有点忙,我读的越来越慢了~继续坚持吧。
在 Python 中,所有函数都是一等对象,整数、字符串和字典都是一等对象(注:first-class object)
要成为一等对象,需要满足
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
5.1 把函数视作对象
def factorial(n):
'''returns n!'''
return 1 if n < 2 else n * factorial(n-1)
result = factorial(5)
print(result) # 120
print(factorial.__doc__) # returns n!
print(type(factorial)) # <class 'function'>
函数对象本身是 function 类的实例
__doc__
是函数对象众多属性中的一个
def factorial(n):
'''returns n!'''
return 1 if n < 2 else n * factorial(n-1)
fact = factorial
print(fact(5))
print(list(map(fact,range(1,5))))
从上面的例子,你看到了2个特点
- 能赋值给变量
- 能作为参数传给函数
有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数
5.2 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)
在函数式编程范式中,最为人熟知的高阶函数有 map、filter、reduce
内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序
示例 5-4
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
map、filter和reduce的现代替代品
列表推导或生成器表达式具有 map 和 filter 两个函数的功能,map 和 filter 还是内置函数
>>> list(map(fact, range(6))) ➊
[1, 1, 2, 6, 24, 120]
>>> [fact(n) for n in range(6)] ➋
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial, filter(lambda n: n % 2, range(6)))) ➌
[1, 6, 120]
>>> [factorial(n) for n in range(6) if n % 2] ➍
[1, 6, 120]
➊ 构建 0! 到 5! 的一个阶乘列表。
➋ 使用列表推导执行相同的操作。
➌ 使用 map 和 filter 计算直到 5! 的奇数阶乘列表。
➍ 使用列表推导做相同的工作,换掉 map 和 filter,并避免了使用 lambda 表达式
map 和 filter 返回生成器(一种迭代器),因此现在它们的直接替代品是生成器表达式
在Python3.9中我看到是一个map、filter对象,当然它也是可以迭代的
reduce在 Python 3 中放到 functools 模块里了。这个函数最常用于求和,自 2003 年发布的 Python 2.3 开始,最好使用内置的 sum 函数
>>> from functools import reduce ➊
>>> from operator import add ➋
>>> reduce(add, range(100)) ➌
4950
>>> sum(range(100)) ➍
4950
sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值
all 和 any 也是内置的归约函数。
all(iterable)
如果 iterable 的每个元素都是真值,返回 True;all([]) 返回 True。
any(iterable)
只要 iterable 中有元素是真值,就返回 True;any([]) 返回 False
5.3 匿名函数
为了使用高阶函数,有时创建一次性的小型函数更便利。这便是匿名函数存在的原因
示例 5-7
改写自5-4
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。
由于句法上的限制,非平凡的 lambda 表达式要么难以阅读,要么无法写出。
如果使用 lambda 表达式导致一段代码难以理解,Fredrik Lundh 建议像下面这样重构。
(1) 编写注释,说明 lambda 表达式的作用。
(2) 研究一会儿注释,并找出一个名称来概括注释。
(3) 把 lambda 表达式转换成 def 语句,使用那个名称来定义函数。
(4) 删除注释。
这几步摘自“Functional Programming HOWTO”(https://docs.python.org/3/howto/functional.
html),这是一篇必读文章
5.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
import asyncio
async def simulate_task1():
await asyncio.sleep(2)
print("Task 1 done")
async def simulate_task2():
await asyncio.sleep(1)
print("Task 2 done")
async def main():
await asyncio.gather(simulate_task1(), simulate_task2())
if __name__ == "__main__":
start_time = time.time()
asyncio.run(main())
end_time = time.time()
print("All tasks completed in", end_time - start_time, "seconds")
与之相对的,下面这个代码就要执行3s
import time
def simulate_task1():
time.sleep(2)
print("Task 1 done")
def simulate_task2():
time.sleep(1)
print("Task 2 done")
if __name__ == "__main__":
start_time = time.time()
simulate_task1()
simulate_task2()
end_time = time.time()
print("All tasks completed in", end_time - start_time, "seconds")
第二个是异步生成器
import asyncio
# 定义一个异步生成器函数
async def async_generator():
for i in range(5):
await asyncio.sleep(1) # 模拟异步操作
yield i
# 使用异步 for 循环来迭代异步生成器
async def main():
async for value in async_generator():
print(f"Received: {value}")
# 运行主函数
asyncio.run(main())
判断对象能否调用,最安全的方法是使用内置的 callable() 函数
>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs, str, 13)]
[True, True, False]
5.5 用户定义的可调用类型
任何 Python 对象都可以表现得像函数。为此,只需实现实例方法
__call__
。
示例
import random
class BingoCage:
def __init__(self, items):
self._items = list(items) # ➊
random.shuffle(self._items) # ➋
def pick(self): # ➌
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') # ➍
def __call__(self): # ➎
return self.pick()
➊ __init__
接受任何可迭代对象;在本地构建一个副本,防止列表参数的意外副作用。
➋ shuffle 定能完成工作,因为 self._items
是列表。
➌ 起主要作用的方法。
➍ 如果 self._items
为空,抛出异常,并设定错误消息。
➎ bingo.pick() 的快捷方式是 bingo()。
bingo = BingoCage(range(5))
print(bingo._items) # [0, 2, 3, 4, 1] # 每次的结果不一样
bingo.pick()
print(bingo._items) # [0, 2, 3, 4]
bingo()
print(bingo._items) # [0, 2, 3]
print(callable(bingo)) # True
实现
__call__
方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间“记住”某些事 [ 例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ]。
创建保有内部状态的函数,还有一种截然不同的方式——使用闭包
装饰器不是可以是类吗?
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Before the function call.")
result = self.func(*args, **kwargs)
print("After the function call.")
return result
@MyDecorator
def my_function():
print("This is my function.")
my_function()
看了下英文的第二版,是这么描述的:Decorators must be callable
,也不知道是改进了,还是中译错了~
5.6 函数内省
>>> def func():pass
...
>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
大多数属性是 Python 对象共有的
函数使用
__dict__
属性存储赋予它的用户属性,这相当于一种基本形式的注解
def userinfo():
'''hello'''
name = 'wuxianfeng'
print(name)
userinfo.age = 18
print(userinfo.__dict__) # {'age': 18} 注意name没有
print(userinfo.__doc__) # hello
Django 框架为函数赋予属性,但不是很常见的做法
# https://docs.djangoproject.com/en/4.2/ref/contrib/admin/
# 摘录一段
def my_property(self):
return self.first_name + " " + self.last_name
my_property.short_description = "Full name of the person"
my_property.admin_order_field = "last_name"
full_name = property(my_property)
示例 5-9 列出常规对象没有而函数有的属性
>>> class Person: pass
...
>>> def func(): pass
...
>>> sorted(set(dir(func)) - set(dir(Person)))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
名称 | 类型 | 说明 |
---|---|---|
__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/) |
5.7 从定位参数到仅限关键字参数
关键字参数(keyword-only argument)
调用函数时使用 * 和 **
展开
可迭代对象,映射到单个参数
示例 5-10 tag 函数用于生成 HTML 标签
使用名为 cls 的关键字参数传入“class”属性,这是一种变通方法,因为“class”是 Python 的关键字
def tag(name, *content, cls=None, **attrs):
"""生成一个或多个HTML标签"""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value) for attr, value in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
print(tag('br')) # 1
print(tag('p', 'hello')) # 2
print(tag('p', 'hello', 'world')) #
print(tag('p', 'hello', id='nodep')) # 3
print(tag('p', 'hello', 'world', 'type',cls='pn')) # 4
print(tag(content='testing', name='img')) # 5
my_tags = {'name': 'img', 'title':'Sunset Boulevard' ,'src': 'sunset.jpg', 'cls': 'framed'}
print(tag(**my_tags)) # 6
➊ 传入单个定位参数,生成一个指定名称的空标签。
➋ 第一个参数后面的任意个参数会被 *content 捕获,存入一个元组。
➌ tag 函数签名中没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。
➍ cls 参数只能作为关键字参数传入。
➎ 调用 tag 函数时,即便第一个定位参数也能作为关键字参数传入。
➏ 在 my_tag 前面加上**
,字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上,余下的则被 **attrs
捕获
输出
<br />
<p>hello</p>
<p>hello</p>
<p>world</p>
<p id="nodep">hello</p>
<p class="pn">hello</p>
<p class="pn">world</p>
<p class="pn">type</p>
<img content="testing" />
<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />
cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。
定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。
如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个 *
示例代码
def f(a, *, b):
print(f'a:{a},b:{b}')
# 前面3个都是对的
f(1,b=2)
f(a=1,b=2)
f(**{'a':1,'b':2})
f(*[1,2]) # 这是错的
f(1,2) # 这是错的
报错
TypeError: f() takes 1 positional argument but 2 were given
第二版中代码是这么写的
def tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
attr_pairs = (f' {attr}="{value}"' for attr, value in sorted(attrs.items()))
attr_str = ''.join(attr_pairs)
if content:
elements = (f'<{name}{attr_str}>{c}</{name}>' for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'
就几个变化
- 参数从cls编程了class_
- attr_str没任何处理,如果不传attr,那你得到的attr_str也是''
- 之前用%s这样的方式,现在用的是f-string
5.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
divmod(x, y, /)
Return the tuple (x//y, x%y). Invariant: div*y + mod == x.
>>> divmod(x=3,y=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments
>>> divmod(3,2)
(1, 1)
意思就很明显了,/
前面的参数必须是位置参数,其后的不管
类似的*
后面的必须是关键字参数
5.8 获取关于参数的信息
这个章节,新版去掉了,放到了这里:https://www.fluentpython.com/extra/function-introspection/
示例 5-12 Bobo 知道 hello 需要 person 参数,并且从 HTTP 请求中获取它
import bobo
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
要安装bobo
然后bobo -f demo.py
# console
Serving ['bobo__main__'] on port 8080...
就可以curl了
C:\Users\songqin008>curl -i http://127.0.0.1:8080
HTTP/1.0 403 Forbidden
Date: Wed, 18 Oct 2023 07:58:05 GMT
Server: WSGIServer/0.2 CPython/3.9.6
Content-Type: text/html; charset=UTF-8
Content-Length: 103
<html>
<head><title>Missing parameter</title></head>
<body>Missing form variable person</body>
</html>
C:\Users\songqin008>curl -i http://127.0.0.1:8080/?person=wuxianfeng
HTTP/1.0 200 OK
Date: Wed, 18 Oct 2023 07:58:27 GMT
Server: WSGIServer/0.2 CPython/3.9.6
Content-Type: text/html; charset=UTF-8
Content-Length: 17
Hello wuxianfeng!
这个时候console
127.0.0.1 - - [18/Oct/2023 15:58:05] "GET / HTTP/1.1" 403 103
127.0.0.1 - - [18/Oct/2023 15:58:27] "GET /?person=wuxianfeng HTTP/1.1" 200 17
跟当代的web框架:flask、Django的输出类似
这个框架是怎么做到知道你要传一个person参数的呢?
bobo.query 装饰器把一个普通的函数(如 hello)与框架的请求处理机制集成起来了
Bobo 会内省 hello 函数,发现它需要一个名为 person 的参数,然后从请求中获取那个名称对应的参数,将其传给
hello 函数,因此程序员根本不用触碰请求对象
怎么做到的呢?
Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?
函数对象有个__defaults__
属性,它的值是一个元组,里面保存着定位参数和关键字参
数的默认值。仅限关键字参数的默认值在__kwdefaults__
属性中。然而,参数的名称在
__code__
属性中,它的值是一个 code 对象引用,自身也有很多属性
def func(name='wuxianfeng',*,age=18):
print(f'{name} is {age} years old!')
print(func.__defaults__) # ('wuxianfeng',)
print(func.__kwdefaults__) # {'age': 18}
print(func.__code__.co_varnames) # ('name', 'age')
__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 | 参数名和局部变量的元组 |
关于函数参数的个数
def func(name,/,type_,*,hometome,age=18,sex='man'):
pass
# 参数数量(不包括仅关键字参数、* 或 ** 参数)
# 此处是 name 和 type_
print(func.__code__.co_argcount)
# 此处是 name
print(func.__code__.co_posonlyargcount)
# 此处是 hometome age sex
print(func.__code__.co_kwonlyargcount)
# 函数一共有几个参数
print(func.__code__.co_argcount+func.__code__.co_kwonlyargcount)
这种做法并不是最便利的
参数名称在
__code__.co_varnames
中,不过里面还有函数定义体中创建的局部变量。因此,参数名称是前 N 个字符串,N 的值由__code__.co_argcount
确定。顺便说一下,这里不包含前缀为 * 或 ** 的变长参数。参数的
默认值只能通过它们在__defaults__
元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来
书中还提供了一个例子,你可以参考
def clip(text, max_len=80):
"""在max_len前面或后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # 没找到空格
end = len(text)
return text[:end].rstrip()
print(clip.__defaults__) # (80,)
print(clip.__code__) # <code object clip at 0x000001EBC00F8EA0, file ...
print(clip.__code__.co_varnames) # ('text', 'max_len', 'end', 'space_before', 'space_after')
更好的方式——使用 inspect 模块
示例 5-17 提取函数的签名
import inspect
def clip(text:str, flag:'biaoji ' = None, max_len:'int > 20'=80):
pass
sig = inspect.signature(clip)
print(sig) # inspect.Signature 对象 # (text, flag = None, max_len=80)
for name ,param in sig.parameters.items():
print(param.kind,":",name,"=",param.default)
print('参数注解:',param.annotation)
(text: str, flag: 'biaoji ' = None, max_len: 'int > 20' = 80)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
参数注解: <class 'str'>
POSITIONAL_OR_KEYWORD : flag = None
参数注解: biaoji
POSITIONAL_OR_KEYWORD : max_len = 80
参数注解: int > 20
inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来
各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty 值表示没有默认值
但要注意=None是默认值,不是_empty
kind的取值可能
class _ParameterKind(enum.IntEnum):
POSITIONAL_ONLY = 0 # 仅限定位参数
POSITIONAL_OR_KEYWORD = 1 #可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)
VAR_POSITIONAL = 2 # 定位参数元组
KEYWORD_ONLY = 3 # 仅限关键字参数
VAR_KEYWORD = 4 # 关键字参数字典
inspect.Parameter 对象还有一个 annotation(注解)属性,它的值通常是 inspect._empty,但是可能包含 Python 3 新的注解句法提供的函数签名元数据
inspect.Signature 对象有个 bind 方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数
import inspect
def clip(text:str, flag:'biaoji ' = None, max_len:'int > 20'=80):
pass
sig = inspect.signature(clip)
sig_params = {'text':'hello','flag':'true','max_len':60}
bound_args = sig.bind(**sig_params)
print(bound_args) # BoundArguments
for name , value in bound_args.arguments.items():
print(name,":",value)
sig_params_2 = {'flag':'true','max_len':60}
bound_args_2 = sig.bind(**sig_params_2)
输出
D:\Python39\python.exe demo.py
Traceback (most recent call last):
File "demo.py", line 11, in <module>
bound_args_2 = sig.bind(**sig_params_2)
File "D:\Python39\lib\inspect.py", line 3062, in bind
return self._bind(args, kwargs)
File "D:\Python39\lib\inspect.py", line 2977, in _bind
raise TypeError(msg) from None
TypeError: missing a required argument: 'text'
<BoundArguments (text='hello', flag='true', max_len=60)>
text : hello
flag : true
max_len : 60
5.9 函数注解
这个章节在第二版中也挪到了typing部分中去
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据
函数声明中的各个参数可以在 : 之后增加注解表达式。
如果参数有默认值,注解放在参数名和 = 号之间。
如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。那个表达式可以是任何类型。
注解中最常用的类型是类(如 str 或 int)和字符串(如'int > 0')
def clip(text:str, flag:'biaoji ' = None, max_len:'int > 20'=80):
pass
print(clip.__annotations__) # {'text': <class 'str'>, 'flag': 'biaoji ', 'max_len': 'int > 20'}
Python 对注解所做的唯一的事情是,把它们存储在函数的
__annotations__
属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用
示例 5-20 从函数签名中提取注解
import inspect
def clip(text:str, flag:'biaoji ' = None, max_len:'int > 20'=80):
pass
sig = inspect.signature(clip)
for param in sig.parameters.values():
print(param.annotation)
<class 'str'>
biaoji
int > 20
函数注解的最大影响或许不是让 Bobo 等框架自动设置,而是为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息
5.10 支持函数式编程的包
operator 和functools 等包的支持,函数式编程风格也可以信手拈来
5.10.1 operator模块
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。我们可以使用 reduce 函数,但是需要一个函数计算序列中两个元素之积
示例 5-21 使用 reduce 函数和一个匿名函数计算阶乘
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
示例 5-22 使用 reduce 和 operator.mul 函数计算阶乘
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式
itemgetter 和 attrgetter 其实会自行构建函数
示例
from operator import itemgetter
a = [1,2,3,4,5]
b = itemgetter(0) # itemgetter(1) 的 作用与 lambda fields: fields[1] 一样:创建一个接受集合的函数,返回索引位 1 上的元素
print(b(a)) # 1 , 类似于a[0]
c = itemgetter(0,2,3) # 如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组:
print(c(a)) # (1,3,4)
再看个示例,结合sorted进行排序
from operator import itemgetter
scores = [
{"语文": 80, "英语": 70},
{"语文": 82, "英语": 78},
{"语文": 86, "英语": 73},
{"语文": 76, "英语": 60}
]
# 使用itemgetter,按照语文成绩排序
x_yuwen = sorted(scores, key=itemgetter("语文"))
# 跟使用lambda 的效果是类似的
x_yingyu = sorted(scores, key=lambda x: x["英语"])
print(x_yuwen)
print(x_yingyu)
书里的示例 5-23 演示使用 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)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),]
from operator import itemgetter
for city in sorted(metro_data, key=itemgetter(1)):
print(city)
根据metro_data中的子元素的第2个元素进行排序
('Sao 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,它构建的函数会返回提取的值构成的元组
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)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),]
from operator import itemgetter
cc_name = itemgetter(1, 0)
for city in metro_data:
print(cc_name(city))
itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现
__getitem__
方法的类
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组
此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性
示例 5-24 定义一个 namedtuple,名为 metro_data
from collections import namedtuple
from operator import itemgetter
from operator import attrgetter
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)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),]
LatLong = namedtuple('LatLong', 'lat long') # 1
Metropolis = namedtuple('Metropolis', 'name cc pop coord') # 2
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) # 3
for name, cc, pop, (lat, long) in metro_data]
print(metro_areas[0])
print(metro_areas[0].coord.lat) # 4
name_lat = attrgetter('name', 'coord.lat') # 5
for city in sorted(metro_areas, key=attrgetter('coord.lat')): # 2
print(name_lat(city)) # 7
➊ 使用 namedtuple 定义 LatLong。
➋ 再定义 Metropolis。
➌ 使用 Metropolis 实例构建 metro_areas 列表;注意,我们使用嵌套的元组拆包提取(lat, long),然后使用它们构建 LatLong,作为 Metropolis 的 coord 属性。
➍ 深入 metro_areas[0],获取它的纬度。
➎ 定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性。
➏ 再次使用 attrgetter,按照纬度排序城市列表。
➐ 使用标号➎中定义的 attrgetter,只显示城市名和纬度。
输出
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
35.689722
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
operator 模块中定义的部分函数
['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains', 'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']
methodcaller。它的作用与 attrgetter和 itemgetter 类似,它会自行创建函数。methodcaller 创建的函数会在对象上调用参数指定的方法
示例 5-25 methodcaller 使用示例:第二个测试展示绑定额外参数的方式
>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME'
>>> hiphenate = methodcaller('replace', ' ', '-')
>>> hiphenate(s)
'The-time-has-come'
第一个测试并不推荐,纯粹的替换
第二个测试就略有意义,它绑定
或称之为冻结
了参数
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)
等价于s.replace(' ','-')
5.10.2 使用functools.partial冻结参数
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定
使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少
示例 5-26 使用 partial 把一个两参数函数改编成需要单参数的可调用对象
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) ➊
>>> triple(7) ➋
21
>>> list(map(triple, range(1, 10))) ➌
[3, 6, 9, 12, 15, 18, 21, 24, 27]
➊ 使用 mul 创建 triple 函数,把第一个定位参数定为 3。
➋ 测试 triple 函数。
➌ 在 map 中使用 triple;在这个示例中不能使用 mul
实际上,mul这个函数必须要传2个参数:乘数和被乘数
当你做了triple = partial(mul, 3)
后,就固定了一个乘数
书中还提供了多个例子
示例 5-27 使用 partial 构建一个便利的 Unicode 规范化函数
>>> import unicodedata, functools
>>> nfc = functools.partial(unicodedata.normalize, 'NFC')
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> s1 == s2
False
>>> nfc(s1) == nfc(s2)
True
示例 5-28 把 partial 应用到示例 5-10 中定义的 tag 函数上
>>> from tagger import tag
>>> tag
<function tag at 0x10206d1e0> ➊
>>> from functools import partial
>>> picture = partial(tag, 'img', cls='pic-frame') ➋
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />' ➌
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', cls='pic-frame') ➍
>>> picture.func ➎
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'cls': 'pic-frame'}
➊ 从示例 5-10 中导入 tag 函数,查看它的 ID。
➋ 使用 tag 创建 picture 函数,把第一个定位参数固定为 'img',把 cls 关键字参数固定为 'pic-frame'。
➌ picture 的行为符合预期
➍ partial() 返回一个 functools.partial 对象。
➎ functools.partial 对象提供了访问原函数和固定参数的属性
functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算
5.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 表达式
5.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 中,这是最早的一门函数式语言
在任何一门语言中,匿名函数都有一个严重的缺点:没有名称。函数有名称,栈跟踪更易于阅读