目录
I have never considered Python to be heavily influenced by functional languages, no matter what people say or think. I was much more familiar with imperative languages such as C and Algol 68 and although I had made functions first-class objects, I didn’t view Python as a functional programming language.
-------Guido van Rossum, Python BDFL
Python 中的函数是一等对象。编程语言理论家将“一等对象”定义为一个满足下述条件的程序实体:
- 在运行时创建;
- 能够分配给数据结构中的变量或元素;
- 作为参数传递给函数;
- 作为函数的结果返回。
整数、字符串和字典是 Python 中一流对象的其他示例——这里没什么特别的。但是,如果您是从一种函数不是一等公民的语言来到 Python 的,那么本章和本书第三部分的其余部分将重点关注将函数视为对象的含义和实际应用。
tip:
“一等函数”一词被广泛用作“函数作为一等对象”的简写。这样并不完美,因为它似乎意味着这是函数间的“精英”。在 Python 中,所有函数都是一等对象。
本章的新内容
“Positional-only parameters”第一版中 “The Nine Flavors of Callable Objects” 部分被命名为“可调用对象的七种风格”。本章提到了分别在 Python 3.5 和 3.6 中引入的原生协程和异步生成器。两者都在第 21 章中进行了介绍,但为了完整起见,此处将它们与其他可调用对象一起提及。
“Positional-only parameters”是一个新部分,涵盖了 Python 3.8 中添加的功能。
我将运行时访问函数注解的覆盖范围移至 “Reading Type Hints at Runtime”。当我写第一版时,PEP 484 — 类型提示仍在讨论中,人们以不同的方式使用注解。从 Python 3.5 开始,注解应符合 PEP 484。因此,讨论它们的最佳位置是在讨论类型提示时。
Note:
第一版有关于函数对象的内省的部分,这些部分太低级并且分散了本章的主题。我将这些部分合并到 fluentpython.com 上的一篇名为 Introspection of Function Parameters 的文章中。
现在让我们看看为什么 Python 函数是成熟的对象。
把函数视作对象
示例 7-1 中的控制台会话显示 Python 函数是对象。这里我们创建一个函数,调用它,读取它的 __doc__ 属性,并检查函数对象本身是否是函数类的实例。
例 7-1。创建并测试一个函数,然后读取它的 __doc__属性, 并检查它的类型
>>> def factorial(n): 1
... '''returns n!'''
... return 1 if n < 2 else n * factorial(n-1)
...
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
>>> factorial.__doc__ 2
'returns n!'
>>> type(factorial) 3
<class 'function'>
- 这是一个控制台会话,因此我们是在运行时创建一个函数
- __doc__是函数对象众多属性的一个
- factorial是function类的一个实例
__doc__ 属性用于生成对象的帮助文本。在 Python 控制台中,命令 help(factorial) 将显示如图 7-1 所示的屏幕内容。
示例 7-2 显示了函数对象的“第一等”性质。我们可以为它分配一个变量fact并通过该名称调用它。
我们也可以将factorial作为参数传递给 map.map 函数返回一个可迭代对象,其中每个元素都是将第一个参数(一个函数)应用于第二个参数(一个可迭代对象)的各个元素的结果,在本例中为 range(10)。
例 7-2。通过不同的名称使用函数,并将函数作为参数传递
>>> fact = factorial
>>> fact
<function factorial at 0x...>
>>> fact(5)
120
>>> map(factorial, range(11))
<map object at 0x...>
>>> list(map(fact, range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
拥有一等函数可以实现函数式编程。函数式编程的标志之一是使用高阶函数,这是我们的下一个主题。
高阶函数
将函数作为参数或返回函数作为结果的函数是高阶函数。其中一个例子是map,如例 7-2 所示。另一个是内置函数 sorted:可选的 key 参数让你提供一个函数来应用于每个元素进行排序,如“list.sort 与 sorted Built-In”中所示。例如,要按长度对单词列表进行排序,只需将 len 函数作为key传递,如例 7-3 所示。
例 7-3。按长度对单词列表进行排序
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>>
任何单参数函数都可以用作key。例如,要创建一个押韵词典,对每个单词反过来拼写并进行排序可能很有用。在示例 7-4 中,请注意列表中的单词根本没有改变;只有它们的反向拼写被用作排序标准,以便浆果一起出现。
例 7-4。按反向拼写对单词列表进行排序
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
在函数式编程范式中,一些最著名的高阶函数是 map、filter、reduce 和 apply。apply 函数在 Python 2.3 中被弃用并在 Python 3 中删除,因为它不再是必需的。如果您需要使用一组不定参数调用函数,则可以编写 fn(*args, **kwargs) 而不是 apply(fn, args, kwargs)。
map、filter 和 reduce 高阶函数仍然存在,但更好的替代方案适用于它们的大多数用例,如下一节所示。
map、filter和reduce的现代替代品
函数式语言通常提供map、filter和reduce高阶函数(有时具有不同的名称)。map 和 filter 函数仍然是 Python 3 的内置函数,但是由于引入了列表推导式和生成器表达式,它们不再那么重要。listcomp 或 genexp 完成map和filter组合的工作,但更具可读性。如示例 7-5。
例 7-5。使用 map 和 filter 生成的阶乘列表与列表推导式的备选方案对比
>>> list(map(fact, range(6))) 1
[1, 1, 2, 6, 24, 120]
>>> [fact(n) for n in range(6)] 2
[1, 1, 2, 6, 24, 120]
>>> list(map(factorial, filter(lambda n: n % 2, range(6)))) 3
[1, 6, 120]
>>> [factorial(n) for n in range(6) if n % 2] 4
[1, 6, 120]
>>>
- 构建阶乘列表0!到 5!。
- 相同的操作,通过列表推导式完成。
- 使用 map 和 filter 的奇数阶乘列表,最多为 5!。
- 列表推导式执行相同的工作,替换 map 和 filter,并使 lambda 表达式变得不再必要。
在 Python 3 中,map 和 filter 返回生成器——迭代器的一种形式——所以它们的直接替代品现在是生成器表达式(在 Python 2 中,这些函数返回列表,因此它们最接近的替代方法是 listcomp)。
reduce 函数从 Python 2 中的内置模块降级为 Python 3 中的 functools 模块。自 2003 年 Python 2.3 发布以来,它最常见的用例求和 最好使用 sum 内置函数。这在可读性和性能方面是一个巨大的胜利(参见示例 7-6)。
例 7-6。使用 reduce 和 sum 执行0- 99 的整数求和
>>> from functools import reduce 1
>>> from operator import add 2
>>> reduce(add, range(100)) 3
4950
>>> sum(range(100)) 4
4950
>>>
- 从python3.0起,reduce不再是内置函数
- 导入add,以免创建一个用来求和的函数
- 计算0-99之和
- 使用sum做相同的求和,无需导入或创建求和函数
Note:sum 和reduce 的共同思想是对序列中的连续项应用某种操作,累加之前的结果,从而将一系列值归约为单个值。
all和any也是内置的归约函数
all(iterable):
如果可迭代对象的每个元素都为真,则返回 True; all([]) 返回 True。
any(iterable)
如果可迭代对象的任何元素为真,则返回 True; any([]) 返回 False。
我在“Vector Take #4: Hashing and a Faster ==”中对reduce进行了更全面的解释,其中一个正在进行的示例为使用此函数提供了有意义的上下文。我在“Vector Take #4: Hashing and a Faster ==”中对reduce进行了更全面的解释,其中一个正在进行的示例为使用此函数提供了有意义的上下文。
要使用高阶函数,有时创建一个小的一次性函数会很方便。这就是匿名函数存在的原因。我们接下来会介绍它们。
匿名函数
lambda 关键字在 Python 表达式中创建匿名函数。
但是,Python 的简单语法将 lambda 函数的主体限制为纯表达式。换句话说,主体不能包含其他 Python 语句,例如 while、try 等。用 = 赋值也是一个语句,所以它不能出现在 lambda 中。可以使用使用 := 的新赋值表达式语法——但如果您需要它,您的 lambda 可能太复杂且难以阅读,应该使用 def 将其重构为常规函数。
匿名函数的最佳用途是在高阶函数的参数列表的上下文中。例如,示例 7-7 是示例 7-4 中的排序押韵单词的示例,用 lambda 重写,没有定义reverse函数。
例 7-7。使用 lambda 按反向拼写对单词列表进行排序
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
除了高阶函数参数的有限上下文之外,匿名函数在 Python 中很少有用。语法限制往往会使非平凡的 lambda 变得不可读或不可行。如果 lambda 难以阅读,我强烈建议您遵循 Fredrik Lund 的重构建议。
FREDRIK LUNDH 的 LAMBDA 重构食谱
如果您发现一段代码因为 lambda 表达式而难以理解,Fredrik Lundh 建议使用以下重构过程:
- 写一条评论来解释 lambda 的作用。
- 研究一下评论,然后想出一个能抓住评论本质的名字。
- 使用该名称将 lambda 转换为 def 语句。
- 删除评论。
这些步骤引用Functional Programming HOWTO,必读
lambda 语法只是语法糖:一个 lambda 表达式创建一个函数对象,就像 def 语句一样。这只是 Python 中几种可调用对象之一。以下部分回顾了所有的可调用对象。
可调用对象的九种风味
用运算符 () 可以应用于用户定义函数和 lambda 之外的其他对象上。要确定对象是否可调用,请使用 callable() 内置函数。 在Python 3.9中,数据模型文档列出了九种可调用类型:
- 用户定义函数 :使用 def 语句或 lambda 表达式创建。
- 内置函数: 用 C 语言实现的函数(用于 CPython),如 len 或 time.strftime。
- 内置方法: 用C语言实现的方法,如 dict.get。
- 方法:在类的主体中定义的函数。
- 类:当被调用时,一个类运行它的 __new__ 方法来创建一个实例,然后 __init__ 来初始化它,最后将实例返回给调用者。因为 Python 中没有 new 运算符,所以调用一个类就像调用一个函数。
- 类实例:如果一个类定义了一个 __call__ 方法,那么它的实例可以作为函数调用——这是下一节的主题。
- 生成器函数:在其主体中使用 yield 关键字的函数或方法。调用时,它们返回一个生成器对象。
- 原生协程函数:用 async def 定义的函数或方法。调用时,它们返回一个协程对象。在 Python 3.5 中添加。
- 异步生成器函数: 使用 async def 定义的函数或方法在其主体中具有 yield。调用时,它们会返回一个异步生成器以供 async for 使用。在 Python 3.6 中添加。
生成器、本地协程和异步生成器函数与其他可调用函数不同,它们的返回值从不是应用程序数据,而是需要进一步处理以生成应用程序数据或执行有用工作的对象。生成器函数返回迭代器。两者都在第 17 章中介绍。原生协程函数和异步生成器函数返回的对象只能在异步编程框架(例如 asyncio)的帮助下工作。它们是第 21 章的主题。
tip:鉴于 Python 中现有的可调用类型的多样性,确定对象是否可调用的最安全方法是使用内置的 callable():
>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs, str, 13)]
[True, True, False]
我们现在继续构建用作可调用对象的类实例。
用户定义的可调用类型
不仅 Python 函数是真正的对象,而且任意 Python 对象也可以表现得像函数一样。只需要实现一个 __call__ 实例方法。
示例 7-8 实现了一个 BingoCage 类。实例由任何可迭代对象构建,并以随机顺序存储内部项目列表。调用实例会取出一个元素。
例 7-8。 bingocall.py:BingoCage 只做一件事:从打乱的列表中取出一个元素
import random
class BingoCage:
def __init__(self, items):
self._items = list(items) 1
random.shuffle(self._items) 2
def pick(self): 3
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage') 4
def __call__(self): 5
return self.pick()
- __init__ 接受任何可迭代对象;构建本地副本可防止对作为参数传递的任何列表产生意外的副作用。
- shuffle 保证可以工作,因为 self._items 是一个列表。
- 起主要作用的方法
- 如果 self._items 为空,则使用自定义消息引发异常。
-
bingo.pick() 的快捷方式:bingo()
这是示例 7-8 的简单演示。请注意bingo实例如何作为函数调用,并且 callable(...) 内置函数将其识别为可调用对象:
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True
实现 __call__ 的类是创建类函数对象的一种简单方法,这些对象具有必须跨调用保持的某些内部状态,例如 BingoCage 中的_items的剩余项。__call__ 的另一个很好的用例是实现装饰器.装饰器必须是可调用的,有时在装饰器调用之间“记住”一些东西(例如,为了记忆——缓存昂贵计算的结果以供以后使用)或将复杂性拆分为单独的方法有时很方便。
创建具有内部状态的函数的函数式方法是使用闭包。闭包和装饰器是第 9 章的主题。
我们现在转到将函数作为对象处理的另一个方面:运行时内省。
函数内省
除了 __doc__ 之外,函数对象还有许多属性。看看 dir 函数揭示我们的factorial函数:
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
>>>
大多数属性对于 Python 对象都是通用的。在本节中,我们将介绍那些与将函数视为对象特别相关的内容,从 __dict__ 开始。
与普通用户定义类的实例一样,函数使用 __dict__ 属性来存储分配给它的用户属性。这作为注释的原始形式很有用。为函数分配任意属性通常不是很常见的做法,但 Django 框架就是这么做的。例如,请参阅 Django 管理站点文档中描述的 short_description、boolean 和 allow_tags 属性。在 Django 文档中,此示例显示将 short_description 附加到方法,以确定使用该方法时将出现在 Django 管理中的记录列表中的描述:
def upper_case_name(obj):
return f"{obj.first_name} {obj.last_name}".upper()
upper_case_name.short_description = 'Customer name'
现在让我们关注函数专有而在通用 Python 用户定义对象中找不到的属性。快速计算两个集合的差为我们提供了特定于函数的专有属性列表(参见示例 7-9)。
例 7-9。列出普通实例中不存在的函数的属性
>>> class C: pass 1
>>> obj = C() 2
>>> def func(): pass 3
>>> sorted(set(dir(func)) - set(dir(obj))) 4
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__',
'__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
>>>
- 创建一个空的用户定义的类
- 创建一个实例
- 定义一个空函数
- 计算差集,并排序,得到函数有而类的实例没有的属性列表
表 7-1 显示了示例 7-9 列出的属性的摘要。
Name | Type | Description |
---|---|---|
|
| Parameter and return type hints |
|
| Implementation of the |
|
| The function closure, i.e., bindings for free variables (often is |
|
| Function metadata and function body compiled into bytecode |
|
| Default values for the formal parameters |
|
| Implementation of the read-only descriptor protocol (see Chapter 24) |
|
| Reference to global variables of the module where the function is defined |
|
| Default values for the keyword-only formal parameters |
|
| The function name |
|
| The qualified function name, e.g., |
在后面的部分和章节中,我们将讨论 __defaults__、__code__ 和 __annotations__ 函数,IDE 和框架使用它们来提取有关函数签名的信息。但是为了充分理解这些属性,我们将绕道而行,探索 Python 提供的用于声明函数参数并将参数传递给它们的强大语法。
从位置参数到仅限关键字参数
Python 函数的最佳特性之一是极其灵活的参数处理机制。密切相关的是当我们调用函数时使用 * 和 ** 将可迭代对象和映射“分解”为单独的参数。要查看这些功能的实际效果,请参阅示例 7-10 的代码和示例 7-11 中显示其用法的测试。
例 7-10。标签生成 HTML 元素;仅关键字参数 class_ 用于传递“class”属性的临时解决方法,因为 class 是 Python 中的关键字
def tag(name, *content, class_=None, **attrs):
"""Generate one or more HTML tags"""
if class_ is not None:
attrs['class'] = class_
if attrs:
attr_pairs = (f' {attr}="{value}"' for attr, value
in sorted(attrs.items()))
attr_str = ''.join(attr_pairs)
else:
attr_str = ''
if content:
elements = (f'<{name}{attr_str}>{c}</{name}>'
for c in content)
return '\n'.join(elements)
else:
return f'<{name}{attr_str} />'
以通过多种方式调用tag函数,如示例 7-11 所示。
例 7-11。示例 7-10 中调用tag函数的多种方法中的一部分
>>> tag('br') 1
'<br />'
>>> tag('p', 'hello') 2
'<p>hello</p>'
>>> print(tag('p', 'hello', 'world'))
<p>hello</p>
<p>world</p>
>>> tag('p', 'hello', id=33) 3
'<p id="33">hello</p>'
>>> print(tag('p', 'hello', 'world', class_='sidebar')) 4
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
>>> tag(content='testing', name="img") 5
'<img content="testing" />'
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'class': 'framed'}
>>> tag(**my_tag) 6
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
- 单个位置参数会生成一个指定name的空标签。
- 第一个参数后的任意个参数都会被*content 捕获为元组
- tag签名中未明确命名的关键字参数被 **attrs 捕获为字典。
- class_ 参数只能作为关键字参数传入。
- 第一个位置参数也可以作为关键字传递。
- my_tag字典加上**前缀会将其所有项作为单独的参数传递,然后绑定到命名参数,其余由 **attrs 捕获。在这种情况下,我们可以在参数字典中有一个 'class' 键,因为它是一个字符串,并且不会与class关键字冲突。
仅限关键字参数是 Python 3 中的一项新功能。在示例 7-10 中, class_ 参数只能作为关键字参数给出——它永远不会捕获未命名的位置参数。要在定义函数时指定仅关键字参数,请在带 * 前缀的参数之后命名它们。如果您不想支持可变位置参数但仍需要仅关键字参数,请在签名中单独放置一个 * ,如下所示:
>>> def f(a, *, b):
... return a, b
...
>>> f(1, b=2)
(1, 2)
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given
请注意,仅关键字参数不需要具有默认值:它们可以强制传入实参,就像前面示例中的 b 一样。
仅限位置参数
从 Python 3.8 开始,用户定义的函数签名可以指定仅限位置参数。这个特性一直存在于内置函数中,例如 divmod(a, b) ,它只能通过位置参数调用,而不能作为 divmod(a=10, b=4) 调用。
要定义仅需要位置参数的函数,请在参数列表中使用 /。这是 Python 3.8 新增功能中的示例,下例展示了如何模拟 divmod 内置函数:
def divmod(a, b, /):
return (a // b, a % b)
/ 左边的所有参数都是位置参数。在 / 之后,您可以指定其他参数,它们照常工作。
Warning :
参数列表中的 / 在Python 3.7 或更早版本中是语法错误。
例如,考虑示例 7-10 中的tag函数。如果我们希望 name 参数仅限位置参数,我们可以在函数签名中在它后面添加一个 / ,如下所示:
def tag(name, /, *content, class_=None, **attrs):
...
您可以在 Python 3.8 的新增功能和 PEP 570 中找到仅限位置参数的其他示例。
在深入研究 Python 的灵活参数声明功能之后,我们回到函数参数的内省,从一个来自 Web 框架的激励示例开始,深入了解内省技术。
检索有关参数的信息
在 Bobo HTTP 微框架中可以找到一个有趣的函数内省应用。要查看实际效果,请考虑示例 7-12 中 Bobo 教程“Hello world”应用程序的变体。
Note: 我提到 Bobo 是因为它率先使用参数自省来减少 Python Web 框架中的样板代码——自 1997 年以来!这种做法现在很普遍。 FastAPI 是使用相同思想的现代框架的一个示例。
例 7-12。 Bobo 知道 hello 需要一个 person 参数,并从 HTTP 请求中检索它
import bobo
@bobo.query('/')
def hello(person):
return f'Hello {person}!'
bobo.query 装饰器将诸如 hello 之类的普通函数与框架的请求处理机制集成在一起。我们将在第 9 章中介绍装饰器——这不是这个例子的重点。关键是 Bobo 内省了 hello 函数,发现它需要一个名为 person 的参数才能工作,它会从请求中检索具有该名称的参数并将其传递给 hello,所以程序员不需要直接处理请求对象。这也使单元测试更容易:无需模拟请求对象来测试 hello 函数。
如果您安装 Bobo 并将其开发服务器启动,执行示例 7-12 中的脚本(例如 bobo -f hello.py),点击 URL http://localhost:8080/ 将产生消息“Missing form variable person ”带有 403 HTTP 代码。发生这种情况是因为 Bobo 知道调用 hello 需要 person 参数,但是在请求中没有找到这样的名字。示例 7-13 是一个使用 curl 的 shell 会话来展示这种行为。
例 7-13。如果请求中缺少函数参数,Bobo 会发出 403 禁止响应; curl -i 将头部转储到标准输出。
$ curl -i http://localhost:8080/
HTTP/1.0 403 Forbidden
Date: Thu, 21 Aug 2014 21:39:44 GMT
Server: WSGIServer/0.2 CPython/3.4.1
Content-Type: text/html; charset=UTF-8
Content-Length: 103
<html>
<head><title>Missing parameter</title></head>
<body>Missing form variable person</body>
</html>
但是,如果访问 http://localhost:8080/?person=Jim,则响应将是字符串“Hello Jim!”。请参见示例 7-14。
示例7-14。 需要传递 person 参数才能得到OK 响应
$ curl -i http://localhost:8080/?person=Jim
HTTP/1.0 200 OK
Date: Thu, 21 Aug 2014 21:42:32 GMT
Server: WSGIServer/0.2 CPython/3.4.1
Content-Type: text/html; charset=UTF-8
Content-Length: 10
Hello Jim!
Bobo 如何知道函数需要哪些参数名称,以及它们是否具有默认值?
在函数对象中, __defaults__ 属性包含一个元组,其中包含位置和关键字参数的默认值。仅关键字参数的默认值出现在 __kwdefaults__ 中。但是,参数的名称可以在 __code__ 属性中找到,该属性是对具有许多自身属性的代码对象的引用。
为了演示这些属性的使用,我们将检查模块 clip.py 中的函数 clip,列在示例 7-15 中。
例 7-15。通过在所需长度附近的空间剪切来缩短字符串的功能
def clip(text, max_len=80):
"""Return text clipped at the last space before or after 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: # no spaces were found
return text.rstrip()
return text[:end].rstrip()
示例 7-16 显示了示例 7-15 中列出的clip函数的 __defaults__、__code__.co_varnames 和 __code__.co_argcount 的值。
例 7-16。提取有关函数参数的信息
>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
>>> clip.__code__.co_argcount
2
如您所见,这不是最方便的信息安排。参数名称出现在 __code__.co_varnames 中,但也包括在函数体中创建的局部变量的名称.因此,参数名称是前 N 个字符串,其中 N 由 __code__.co_argcount 给出,顺便说一下,它不包括任何以 * 或 ** 为前缀的变量参数。默认值仅通过它们在 __defaults__ 元组中的位置来标识,因此要将每个值与相应的参数链接起来,您必须从最后一个扫描到第一个。在示例中,我们有两个参数,text 和 max_len,一个默认值 80,因此它必须属于最后一个参数 max_len。这很尴尬。
幸运的是,有一个更好的方法:inspect 模块。
看看示例 7-17。
>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig # doctest: +ELLIPSIS
<inspect.Signature object at 0x...>
>>> str(sig)
'(text, max_len=80)'
>>> for name, param in sig.parameters.items():
... print(param.kind, ':', name, '=', param.default)
...
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80
这样就好多了。 inspect.signature 返回一个inspect.Signature 对象,该对象具有一个parameters属性,可让您读取名称到inspect.Parameter 对象的有序映射。每个 Parameter 实例都具有名称、默认值和种类等属性。特殊值 inspect._empty 表示没有默认值的参数,考虑到 None 是有效且流行的默认值,这样是合理的。
kind 属性包含来自 _ParameterKind 类的五个可能值之一:
- POSITIONAL_OR_KEYWORD :可以作为位置参数或关键字参数传递的参数(大多数 Python 函数参数都是这种类型)。
- VAR_POSITIONAL :位置参数的元组。
- VAR_KEYWORD :关键字参数的字典。
- KEYWORD_ONLY :仅限关键字参数(Python 3 中的新增功能)。
- POSITIONAL_ONLY :仅限位置参数; Python 3.8 之前的函数声明语法不受支持,但以 C 语言实现的现有函数为例,例如 divmod,不接受关键字传递的参数。
除了name、default和kind之外,inspect.Parameter 对象还有一个annotation属性,该属性的值通常是 inspect._empty,但可能包含通过 Python 3 中的新注释语法提供的函数签名元数据(在第 8 章中介绍)。
一个inspect.Signature 对象有一个bind 方法,它接受任意数量的参数并将它们绑定到签名中的参数,应用将实际参数与形式参数匹配的常用规则。框架可以使用它来在实际函数调用之前验证参数。示例 7-18 展示了如何操作。
例 7-18。将示例 7-10 中标记函数的函数签名绑定到参数字典
>>> import inspect
>>> sig = inspect.signature(tag) 1
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'cls': 'framed'}
>>> bound_args = sig.bind(**my_tag) 2
>>> bound_args
<inspect.BoundArguments object at 0x...> 3
>>> for name, value in bound_args.arguments.items(): 4
... print(name, '=', value)
...
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
>>> del my_tag['name'] 5
>>> bound_args = sig.bind(**my_tag) 6
Traceback (most recent call last):
...
TypeError: 'name' parameter lacking default value
- 从示例 7-10 中的标签函数获取tag。
- 将参数字典传递给 .bind()。
- 生成一个 inspect.BoundArguments 对象。
- 迭代 bound_args.arguments 中的元素,它是一个 OrderedDict,以显示参数的名称和值。
- 从 my_tag 中删除强制参数name。
- 调用 sig.bind(**my_tag) 会引发 TypeError 缺少 name 参数的异常。
此示例显示 Python 数据模型如何在inspect的帮助下公开解释器用于将参数绑定到函数调用中的形式参数的相同机制。框架和 IDE 等工具可以使用此信息来验证代码。
在检查了函数对象的解剖结构之后,本章的其余部分将介绍标准库中用于函数式编程的最有用的包。
函数式编程包
尽管 Guido 明确表示他并没有将 Python 设计为一种函数式编程语言,但可以很好地使用函数式编码风格,感谢一等函数和对 operator 和 functools 等包的支持,我们将在接下来的两节中介绍。
operator模块
通常在函数式编程中,将算术运算符用作函数是很方便的。例如,假设您想在不使用递归的情况下将一系列数字相乘以计算阶乘。要执行求和,您可以使用 sum,但没有用于乘法的等效函数。您可以使用reduce——正如我们在“map、filter 和reduce 的现代替换”中看到的那样——但这需要一个函数来将序列的两个项目相乘。示例 7-19 展示了如何使用 lambda 解决这个问题。
例 7-19。使用 reduce 和匿名函数实现的阶乘
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
operator模块为数十个运算符提供等价的函数,因此您不必编写诸如 lambda a, b: a*b 之类的琐碎函数。有了它,我们可以将示例 7-19 重写为示例 7-20。
例 7-20。使用 reduce 和 operator.mul 实现的阶乘
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
运算符替换的另一组单一技巧 lambda 是从序列中选择项目或从对象读取属性的函数:itemgetter 和 attrgetter 是构建自定义函数来执行此操作的工厂。示例 7-21 展示了 itemgetter 的一种常见用法:按一个字段的值对元组列表进行排序。在该示例中,城市按国家/地区代码(字段 1)排序打印。本质上,itemgetter(1) 创建了一个函数,给定一个集合,返回索引 1 处的项目。 这比 lambda 字段更容易编写和读取:fields[1],它的作用相同:
例 7-21。对元组列表进行排序的 itemgetter 演示(示例 2-8 中的数据)
>>> 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)
...
('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,它构建的函数将返回包含提取值的元组,这对于对多个键进行排序非常有用:
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')
>>>
因为 itemgetter 使用 [] 运算符,它不仅支持序列,还支持映射和任何实现 __getitem__ 的类。
itemgetter 的兄弟是 attrgetter,它创建函数以按名称提取对象属性。果您将多个属性名称作为参数传递给 attrgetter,它还会返回一个值元组。如果您将多个属性名称作为参数传递给 attrgetter,它还会返回一个值元组。此外,如果任何参数名称包含 . (点),attrgetter 在嵌套对象中导航以检索属性。这些行为如示例 7-22 所示。这不是最短的控制台会话,因为我们需要构建一个嵌套结构来展示 attrgetter 对点属性的处理。
例 7-22。 attrgetter 的演示,用于处理先前定义的名为 Metro_data 的命名元组列表(与示例 7-21 中出现的列表相同)
>>> from collections import namedtuple
>>> LatLon = namedtuple('LatLon', 'lat lon') 1
>>> Metropolis = namedtuple('Metropolis', 'name cc pop coord') 2
>>> metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon)) 3
... for name, cc, pop, (lat, lon) in metro_data]
>>> metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLon(lat=35.689722,
lon=139.691667))
>>> metro_areas[0].coord.lat 4
35.689722
>>> from operator import attrgetter
>>> name_lat = attrgetter('name', 'coord.lat') 5
>>>
>>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): 6
... print(name_lat(city)) 7
...
('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
- 使用namedtuple定义LatLong
- 定义namedtuple定义Metropolis
- 使用 Metropolis 实例构建 Metro_areas 列表;注意嵌套元组解包以提取 (lat, lon) 并使用它们为 Metropolis 的 coord 属性构建 LatLon。
- 进入元素metro_areas[0] 以获取其纬度
- 定义一个attrgetter来检索name和 coord.lat 嵌套属性。
- 再次使用 attrgetter 按纬度对城市列表进行排序。
- 使用5中定义的attrgetter仅显示城市名称和纬度。
这是 operator 中定义的部分函数列表(以 _ 开头的名称被省略,因为它们主要是实现细节):
>>> [name for name in dir(operator) if not name.startswith('_')]
['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']
列出的 54 个名字中的大多数是不言而喻的。以 i 为前缀的名称组和另一个运算符的名称(例如 iadd、iand 等)对应于扩充赋值运算符(例如 +=、&= 等)。如果第一个参数是可变的,就会就地改变第一个参数;如果没有,该函数的工作方式与不带 i 前缀的函数类似:它只返回操作的结果。
在剩余的运算符函数中,methodcaller 是我们将介绍的最后一个。它有点类似于 attrgetter 和 itemgetter,因为它可以动态创建一个函数。它创建的函数会在对象上按名称调用参数指定的方法,如例 7-23 所示。
例 7-23。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'
Example 7-23 中的第一个测试只是为了显示方法调用者在工作,但是如果您需要将 str.upper 用作函数,则可以在 str 类上调用它并传递一个字符串作为参数,如下所示:
>>> str.upper(s)
'THE TIME HAS COME'
示例 7-23 中的第二个测试表明,methodcaller 也可以执行部分应用程序来冻结某些参数,就像 functools.partial 函数一样。那是我们的下一个主题。
使用 functools.partial 冻结参数
functools 模块提供了几个高阶函数。其中最著名的可能是reduce,它在“map、filter 和reduce 的现代替换”中有所介绍。在 functools 中剩余的函数中,最有用的是 partial 及其变体,partialmethod。
functools.partial 是一个允许部分应用函数的高阶函数。给定一个函数,部分应用生成一个新的可调用对象,其中原始函数的一些参数是固定的。这对于将带有一个或多个参数的函数调整为需要具有较少参数的回调的 API 非常有用。示例 7-24 是一个简单的演示。
例 7-24。使用 partial 把一个两个参数的函数改编成单参数的可调用对象。
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) 1
>>> triple(7) 2
21
>>> list(map(triple, range(1, 10))) 3
[3, 6, 9, 12, 15, 18, 21, 24, 27]
- 从 mul 创建新的triple函数,将第一个位置参数绑定到 3。
- 测试triple函数
- 在map中使用triple,在这个示例中不能使用mul
一个更有用的例子涉及我们在“Normalizing Unicode for Reliable Comparisons”中看到的 unicode.normalize 函数。如果您使用多种语言的文本,您可能希望在比较或存储之前将 unicode.normalize('NFC', s) 应用于任何字符串 s。如果您经常这样做,那么使用 nfc 函数会很方便,如示例 7-25 所示。
例 7-25。使用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
partial 将 callable 作为第一个参数,然后是任意数量的要绑定的位置和关键字参数。示例 7-26 显示了使用 partial 和示例 7-10 中的tag函数来冻结一个位置参数和一个关键字参数。
例 7-26。示例 7-10 中应用于函数标记的partial演示
>>> from tagger import tag
>>> tag
<function tag at 0x10206d1e0> 1
>>> from functools import partial
>>> picture = partial(tag, 'img', cls='pic-frame') 2
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />' 3
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', cls='pic-frame') 4
>>> picture.func 5
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'cls': 'pic-frame'}
- 从示例 7-10 导入tag并显示其 ID。
- 通过使用 'img' 固定第一个位置参数和使用 'pic-frame' 固定 cls 关键字参数,从tag创建picture函数。
- picture按预期工作
- partial() 返回一个 functools.partial 对象
- functools.partial 对象具有提供对原始函数和固定参数的访问的属性。
functools.partialmethod 函数(自 Python 3.4 起)与 partial 执行相同的工作,但旨在与方法一起使用。
一个令人印象深刻的 functools 函数是 lru_cache,它进行记忆化——一种通过存储函数调用的结果来避免昂贵的重新计算的优化。我们将在第 9 章中介绍它,其中解释了装饰器,以及设计用作装饰器的其他高阶函数:singledispatch 和 wraps。