《流畅的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 a
coroutine 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} />'

就几个变化

  1. 参数从cls编程了class_
  2. attr_str没任何处理,如果不传attr,那你得到的attr_str也是''
  3. 之前用%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 中,这是最早的一门函数式语言

在任何一门语言中,匿名函数都有一个严重的缺点:没有名称。函数有名称,栈跟踪更易于阅读

posted @ 2023-10-25 15:53  博客已废弃  阅读(64)  评论(0编辑  收藏  举报