Python的元组也是不可变对象,因此很适合函数式编程。元组数据结构可用的方法不多,大多数处理都是通过前缀式语法完成的。元组应用广泛,主要用于元组列表、嵌套元组和元组生成器。
命名元组这个类为元组增加了一个重要特征:通过属性名称而非序号引用属性。我们可以使用命名元组创建数据累积式的对象,这样就能基于无状态对象创建纯函数,同时保持数据以对象形式有序地组织在一起了。
后续将主要使用元组和命名元组处理集合数据。对于单个或者两个独立值,可以采用函数的命名参数;而当处理集合数据时,则需要使用可迭代元组或者可迭代命名元组。
使用元组还是命名元组取决于具体场景中使用哪种数据结构更方便,例如可以把包含红、绿、蓝三种颜色的数据抽象为形如(number, number, number)
的三元组,但这个定义没有红、绿、蓝的顺序。有几种方法可以让元组的结构更清晰。
可以通过下面这个函数从该三元组中抽取数据来表明其结构。
red = lambda color: color[0]
green = lambda color: color[1]
blue = lambda color: color[2]
对于元组item
,可以使用red(item)
从中选出红色成分,并添加更正式的类型标示。
from typing import Tuple, Callable
RGB = Tuple[int, int, int]
red: Callable[[RGB], int] = lambda color: color[0]
这里定义了代表三元组的新类型RGB
。red
的类型标示是Callable[[RGB], int]
,表示它是一个函数,输入参数类型为RGB
,返回一个整型值。
或者使用旧式命名元组来定义:
from collections import namedtutple
Color = namedtuple("Color", ("red", "green", "blue", "name"))
更好的方法是使用typing
模块中的NamedTuple
类:
from typing import NamedTuple
class Color(NamedTuple):
"""An RGB color."""
red: int
green: int
blue: int
name: str
Color
类定义了元组中每个位置的名字和类型标示,在保持性能优势和不可变性的基础上,还可以利用mypy校验该类型变量的使用方法是否恰当。
通过上面两种方法提取红色时,可以用item.red
代替red(item)
,它们都比item[0]
可读性好。
元组在函数式编程中的应用侧重于可迭代元组设计模式。
使用生成器表达式
前面给出了一些生成器表达式的示例,稍后会给出更多生成器表达式,本节将介绍关于生成器的一些高级技术。
生成器表达式常用于通过列表推导或者字典推导生成列表或者字典字面量。以[x**2 for x in range(10)]
为例,它是一个列表推导,也称列表展示。列表展示是生成器表达式的一种用法,集合(列表或字典)展示包含闭合的字面语法,这里是用列表的字面语法[]
包裹生成器[x**2 for x in range(10)]
,表示这是一个列表推导,返回一个由其中包含的生成器表达式x**2 for x in range(10)
生成的列表对象。本节主要介绍生成器表达式,不涉及列表对象。
集合对象和生成器表达式都是可迭代的,因此许多行为相似,但稍后会看到,它们并不等同。使用展示的缺点是会生成一个(可能比较大的)集合对象,而生成器表达式是惰性的,按需创建值,因而可提升性能。
对于生成器表达式,需要说明两点:
- 除了被一些特殊函数(例如
len()
等需要知道整个集合大小的函数)使用时,生成器的行为与序列类似; - 生成器只能使用一次,可将使用完的生成器视为空的。
下面是一个后续示例会用到的生成器函数:
def pfactorsl(x: int) -> Iterator[int]:
if x % 2 == 0:
yield 2
if x // 2 > 1:
yield from pfactorsl(x // 2)
return
for i in range(3, int(math.sqrt(x) + .5) + 1, 2):
if x % i == 0:
yield i
if x // i > 1:
yield from pfactorsl(x // i)
return
yield x
该函数计算输入值的所有质因数。如果输入值 x 是偶数,则返回2,然后递归计算出 x/2 的所有质因数。
对于奇数,从3开始依次验证每个奇数是不是输入值的因数,如果是,返回该因数 i ,然后递归生成 x\divi 的所有质因数。
如果输入值没有质因数,它本身一定是质数,会返回它自身。
这里2作为一个特殊值,每次可以将迭代次数减半,因为2以外的所有质数都是奇数。
除了递归,上面的函数还使用了for
for
循环外使用循环变量i,因此当修改循环体时,它有状态的特点并不会带来麻烦。
这个例子展示了如何手动实现尾递归优化,用循环代替了从3到 \sqrt{x} 的递归求值,避免了深度嵌套的递归调用。
由于函数是可迭代的,用yield from
语句从递归调用中计算出所有值,返回给调用者。
在递归生成器函数中要谨慎使用
return
语句,不要用下面的命令行:
Return recursive_iter(args)
它只返回生成器对象,而不对函数求值并返回结果,下面两种写法可行:
for result in recursive_iter(args):
yield result
或者
yield from recursive_iter(args)
上面函数的纯递归版本如下:
def pfactorsr(x: int) -> Iterator[int]:
def factor_n(x: int, n: int) -> Iterator[int]:
if n*n > x:
yield x
return
if x % n == 0:
yield n
if x//n > 1:
yield from factor_n(x//n, n)
else:
yield from factor_n(x, n+2)
if x % 2 == 0:
yield 2
if x//2 > 1:
yield from pfactorsr(x//2)
return
yield from factor_n(x, 3)
首先定义内部递归函数factor_n()
测试 3\leqn\leq \sqrt{x} 范围内 n 是否为 x的因数,如果待测的n不在此范围内,说明x是质数。否则检查 n 是否是 x 的因数,如果是,返回 n,并递归生成 \frac{x}{n} 的所有质因数,否则递归测试 n+2 是否是 x 的因数,这里需要递归测试(n+2, n+2+2, n+2+2+2, …) 中的所有值,虽然它在写法上比前面的for
循环版本简单,但由于Python栈长度限制,无法处理因数个数超过1000的情况。
外层函数处理边界情况。与其他质数处理算法一样,这里将2作为特殊情形,对于偶数,首先返回2,然后递归生成 x \div 2 的质因数。其他质因数一定是不小于3的奇数,所以从3开始用factor_n()
函数依次测试每个可能的解。
纯递归函数最多能处理约4 000 000个输入值。超过这个值,计算会由于超出Python的嵌套递归深度上限而失败。
生成器的局限
生成器表达式和生成器函数是有局限的,如下所示:
>>> from ch02_ex4 import *
>>> pfactorsl(1560)
<generator object pfactorsl at 0x1007b74b0>
>>> list(pfactorsl(1560))
[2, 2, 2, 3, 5, 13]
>>> len(pfactorsl(1560))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'generator' has no len()
第一个例子表明生成器函数不是严格求值的,是惰性的,在外部对其求值之前,并不进行实际的计算。这不是局限,而是将生成器表达式用于Python函数式编程的原因。
第二个例子通过实例化生成器函数得到一个列表对象,便于观察输出和编写单元测试用例。
第三个例子体现了生成器函数的一个局限:不能使用len()
函数求其长度。因为生成器是惰性的,所以在所有值都取完之前,无法知道值的总数。
生成器函数的另一个局限是只能使用一次,如下所示:
>>> result = pfactorsl(1560)
>>> sum(result)
27
>>> sum(result)
0
第一次使用sum()
函数对生成器求值后,第二次再使用时生成器为空了,表明生成器只能用一次。
Python中的生成器是有生命周期的,从函数式编程的角度看这不是特别理想。
可以使用itertools.tee()
函数弥补只能用一次的限制,下面是一个简单的示例:
import itertools
from typing import Iterable, Any
def limits(iterable: Iterable[Any]) -> Any:
max_tee, min_tee = itertools.tee(iterable, 2)
return max(max_tee), min(min_tee)
这为作为参数传入的生成器表达式创建了两个副本:max_tee()
和min_tee()
,保持原始生成器不变,借助这个功能,可以非常灵活地把函数组合在一起。通过对两个副本求值,得到生成器计算结果的最大值和最小值。
可迭代对象中的值取完后,将不再生成任何值。当需要对生成器进行多种归约时,例如求和、计数、求最大值或最小值等,请注意,设计算法时必须考虑到只能用一次这个特点。
组合生成器表达式
函数式编程的核心特征是:通过灵活地组合生成器表达式和生成器函数描述复杂的处理流程。下面介绍使用生成器表达式时几种常用的组合方法。
第一种方法是通过创建复合函数来组合生成器。假设有个生成器(f(x) for x in range())
,如果需要计算g(f(x))
,可用以下几种方法来组合它们。
可以把原来的生成器表达式改成如下形式:
g_f_x = (g(f(x)) for x in range())
这种方式从技术上说是正确的,但违背了复用原则:没有复用已有的表达式,而是改写了。
也可将一个表达式嵌入另一个表达式中,如下所示:
g_f_x = (g(y) for y in (f(x) for x in range()))
这样就可以方便地进行变量替换了。可通过如下命令实现复用:
f_x = (f(x) for x in range())
g_f_x = (g(y) for y in f_x)
这种实现方法的优点是无须改写原来的表达式(f(x) for x in range())
,只需把它赋给一个变量即可。
组合后的表达式仍然是生成器表达式,并且是惰性的。当从g_f_x
中取出一个值时,它会从f_x
中取一个值,f_x
再从range()
函数中取一个值。