Python函数式编程,常用模块 operator 、functools
虽然Python 不是函数式编程语言,但是得益于一等函数、模式匹配,以及 operator 和 functools 等包的支持,函数式编程也是手到擒来。
operator 模块
下面是 operator 模块中定义的部分函数列表
import operator
print([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']
大部分函数名称的作用不言而喻。
以 i 开头、后面是另一个运算符的那些名称(例如 iadd、iand 等),对应的是增量赋值运算符(例如 +=、&= 等)。如果第一个参数是可变的,那么这些函数就会就地修改第一个参数;否则,作用与不带 i 的函数一样,直接返回运算结果。
应用1:算术运算函数
在函数式编程中,经常需要把算术运算符当作函数使用。
例如,不使用递归计算阶乘。求和可以使用 sum 函数,求积则没有这样的函数。
通常使用 reduce 函数,但是需要一个函数来计算序列中两项之积。
使用 reduce 函数和lambda匿名函数计算阶乘
from functools import reduce
def factorial(n):
return reduce(lambda a, b: a*b, range(1, n+1))
print(factorial(5))
>>>
120
reduce(function, iterable[, initializer])
参数
- function – 函数,有两个参数
- iterable – 可迭代对象
- initializer – 可选,初始参数
函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
匿名函数
lambda
关键字使用 Python 表达式创建匿名函数
lambda 函数的主体只能是纯粹的表达式,函数的主体中不能有while
、try
等语句和=
赋值。
可以用:=
赋值表达式。不过,有这种赋值表达式的 lambda 函数可能太过复杂,可读性低,因此建议重构,改成使用 def 定义的常规函数。
除了作为参数传给高阶函数,Python 很少使用匿名函数。
operator 模块为多个算术运算符提供了对应的函数,无须再动手编写像 lambda a, b: a*b
这样的匿名函数。
使用 reduce 函数和 operator.mul 函数计算阶乘
from functools import reduce
from operator import mul
def factorial(n):
return reduce(mul, range(1, n+1))
print(factorial(5))
>>>
120
应用2:从序列中取出项或读取对象属性
operator 模块中还有一类函数,即工厂函数 itemgetter
和 attrgetter
,能替代从序列中取出项或读取对象属性的 lambda 表达式。
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)),
('São Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
from operator import itemgetter
# 按照国家代码(第 2 个字段)的顺序打印各个城市的信息
for city in sorted(metro_data, key=itemgetter(1)):
print(city)
>>>
('São 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(1)
会创建一个接受容器的函数,返回索引位 1 上的项。与作用相同的 lambda fields: fields[1] 相比,使用 itemgetter 更容易写出代码,而且可读性更高
如果传给itemgetter
多个索引参数,那么 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', 'São Paulo')
itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现 __getitem__
方法的类。
attrgetter
attrgetter创建的函数会根据名称来提取对象的属性。如果传给 attrgetter 多个属性名,那么它也会返回由提取的值构成的元组。此外,如果参数名中包含 .(点号),那么 attrgetter 就会深入嵌套对象,检索属性。
# 构造具名元组
from collections import namedtuple
LatLon = namedtuple('LatLon', 'lat lon')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon))
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
>>>
35.689722
使用 attrgetter
处理具名元组
from operator import attrgetter
#定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性
#如果传给 attrgetter 多个属性名,那么它也会返回由提取的值构成的元组。
name_lat = attrgetter('name', 'coord.lat')
for city in sorted(metro_areas, key=attrgetter('coord.lat')):
print(name_lat(city))
>>>
('São Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)
methodcaller
作用与 attrgetter 和 itemgetter 类似。methodcaller 创建的函数会在对象上调用参数指定的方法
from operator import methodcaller
s = 'The time has come'
upcase = methodcaller('upper')
upcase(s)
>>>
'THE TIME HAS COME'
如果想把 str.upper 作为函数使用,则只需在 str 类上调用,传入一个字符串参数即可。上述方法等价于·
str.upper(s)
methodcaller 还可以冻结某些参数,这与 functools.partial
函数的作用类似
hyphenate = methodcaller('replace', ' ', '-')
hyphenate(s)
>>>
'The-time-has-come'
functools模块
functools 模块提供了一系列高阶函数,比如上面用过的 reduce
。
另外一个值得关注的函数是 partial
,它可以根据提供的可调用对象产生一个新可调用对象,为原可调用对象的某些参数绑定预定的值。
使用这个函数可以把接受一个或多个参数的函数改造成需要更少参数的回调的 API。
使用 functools.partial 冻结参数,把一个双参数函数改造成只需要一个参数的可调用对象
from operator import mul
from functools import partial
triple = partial(mul, 3) # 函数 3*num
triple(7)
>>>
21
list(map(triple, range(1, 10)))
>>>
[3, 6, 9, 12, 15, 18, 21, 24, 27]
partial 的第一个参数是一个可调用对象,后面跟着任意个要绑定的位置参数和关键字参数。
这里在另一个文章中定义的 tag 函数上使用 partial 冻结了一个位置参数和一个关键字参数。
# tag 函数用于生成 HTML 标签。
# 可以使用名为 class_ 的仅限关键字参数传入“class”属性,这是一种变通方法,因为“class”是 Python 中的关键字
def tag(name, *content, class_=None, **attrs):
"""生成一个或多个HTML标签"""
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} />'
from functools import partial
picture_tag = partial(tag, 'img', class_='pic-frame')
picture_tag(src='wumpus.jpeg')
>>>
'<img class="pic-frame" src="wumpus.jpeg" />'
==============================
picture_tag
>>>
functools.partial(<function tag at 0x000001AF1EA98700>, 'img', class_='pic-frame')
==============================
picture_tag.func
>>>
<function tag at 0x10206d1e0>
==============================
picture_tag.args
>>>
('img',)
==============================
picture_tag.keywords
>>>
{'class_': 'pic-frame'}
functools.partialmethod
函数的作用与 partial 一样,不过其用于处理方法。
functools 模块中还有一些高阶函数可用作函数装饰器,例如 cache
、singledispatch
等。
后面有时间再学习。