《流畅的Python》卢西亚诺·拉马略 第5章 一等函数 读书笔记
目录
5.10.2 使用functools.partial冻结参数
在 Python 中,函数是一等对象。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
在 Python 中,整数、字符串和字典都是一等对象。
人们经常将“把函数视作一等对象”简称为“一等函数”。
5.1 把函数视作对象
>>> def factorial(n):
... '''returns n!'''
... return 1 if n < 2 else n * factorial(n-1)
...
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
>>>
>>> type(factorial)
<class 'function'>
>>>
通过别的名称使用函数,再把函数作为参数传递
>>> fact = factorial
>>> fact
<function factorial at 0x7f8f484de940>
>>>
>>> fact(5)
120
>>>
5.2 高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher order function)。map 函数就是一例;内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个
元素上进行排序。
【例】根据单词的长度排序,只需把 len 函数传给 key 参数
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
>>>
任何单参数函数都能作为 key 参数的值。例如,为了创建押韵词典,可以把各个单词反过来拼写,然后排序。
>>> def reverse(word):
... return word[::-1]
...
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
在函数式编程范式中,最为人熟知的高阶函数有map、filter、reduce。
在 Python 3 中,map 和 filter 还是内置函数。列表推导或生成器表达式具有 map 和 filter 两个函数的功能,而且更易于阅读。
【例】计算阶乘列表:map 和 filter 与列表推导比较
>>> map(factorial,range(11))
<map object at 0x7f8f485088e0>
>>>
>>> list(map(fact,range(11)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
>>>
>>> [fact(n) for n in range(11)]
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
>>>
>>> list(map(fact,filter(lambda n: n%2, range(6))))
[1, 6, 120]
>>>
>>> [fact(n) for n in range(6) if n%2]
[1, 6, 120]
>>>
说明:在 Python 3 中,map 和 filter 返回生成器(一种迭代器),因此现在它们的直接替代品是生成器表达式。
在 Python 2 中,reduce 是内置函数,但是在 Python 3 中放到functools 模块里了,这个函数最常用于求和。
【例】使用 reduce 和 sum 计算 0~99 之和
>>> from functools import reduce
>>> from operator import add
>>> reduce(add, range(100))
4950
>>>
all 和 any 也是内置的归约函数
all(iterable) 如果 iterable 的每个元素都是真值,返回 True;all([]) 返回True。
any(iterable) 只要 iterable 中有元素是真值,就返回 True;any([]) 返回False。
5.3 匿名函数
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。在参数列表中最适合使用它。
【例】使用 lambda 表达式反转拼写,然后依此给单词列表排序
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
5.4 可调用对象
除了用户定义的函数,调用运算符 -> 即 () 还可以应用到其它对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象。
(1)用户定义的函数
使用 def 语句或 lambda 表达式创建。
(2)内置函数
使用 C 语言(CPython)实现的函数,如 len 或 time.strftime。
(3)内置方法
使用 C 语言实现的方法,如 dict.get。
(4)方法
在类的定义体中定义的函数。
(5)类
调用类时会运行类的 __new__ 方法创建一个实例,然后运行__init__ 方法,初始化实例,最后把实例返回给调用方。
(6)类的实例
如果类定义了 __call__ 方法,那么它的实例可以作为函数调用。
(7)生成器函数
使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象。
Python 中有各种各样可调用的类型,因此判断对象能否调用,最安全的方法是使用内置的 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__。
【例】bingocall.py, BingoCage 类的实例使用任何可迭代对象构建,而且会在内部存储一个随机顺序排列的列表。
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__接受任何可迭代对象
bingo.pick()的快捷方式是bingo()
5.6 函数内省
使用 dir 函数可以探知factorial 具有下述属性,其中大多数属性是 Python 对象共有的
>>> def factorial(n):
... '''returns n!'''
... return 1 if n < 2 else n * factorial(n-1)
...
>>>
>>> dir(factorial)
['__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__']
>>>
【例】列出常规对象没有而函数有的属性
>>> class C: pass
...
>>> obj = C()
>>> def func(): pass
...
>>> sorted(set(dir(func)) - set(dir(obj)))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
>>>
5.7 从定位参数到仅限关键字参数
示例 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)
示例 5-11 tag 函数众多调用方式中的几种
>>> tag('br') #1
'<br />'
>>> tag('p', 'hello') #2
'<p>hello</p>'
>>> tag('p', 'hello', 'world') #2
'<p>hello</p>\n<p>world</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', cls='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', 'cls':'framed'}
>>> tag(**my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
>>>
说明:
#1 传入单个定位参数,生成一个指定名称的空标签。
#2 第一个参数后面的任意个参数会被 *content 捕获,存入一个元组。
#3 没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。
#4 cls 参数只能作为关键字参数传入。
#5 调用 tag 函数时,即便第一个定位参数也能作为关键字参数传入。
#6 在 my_tag 前面加上 **,字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上,余下的则被 **attrs 捕获。
仅限关键字参数是 Python 3 新增的特性。在示例 5-10 中,cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。
定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。
如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个 *,如下所示:
>>> def f(a, *, b):
... return a,b
...
>>> f(1,2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given
>>> f(1,b=2)
(1, 2)
>>>
注意,仅限关键字参数不一定要有默认值,可以像上例中 b 那样,强制必须传入实参
5.10 支持函数式编程的包
5.10.1 operator模块
在函数式编程中,经常需要把算术运算符当作函数使用。例如,不使用递归计算阶乘。求和可以使用 sum 函数,但是求积则没有这样的函数。
我们可以使用 reduce 函数,但是需要一个函数计算序列中两个元素之积。
(1)mul
示例 5-21 使用 reduce 函数和一个匿名函数计算阶乘
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
operator 模块为多个算术运算符提供了对应的函数,从而避免编写匿名函数。
示例 5-22 使用 reduce 和 operator.mul 函数计算阶乘
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
(2)itemgetter
itemgetter 的常见用途:根据元组的某个字段给元组列表排序。在如下示例中,按照国家代码(第 2 个字段)的顺序打印各个城市的信息。其实,itemgetter(1) 的作用与 lambda fields: fields[1] 一样,创建一个接受集合的函数,返回索引位 1 上的元素。
示例 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)
...
('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__ 方法的类。
(3)attrgetter
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。
示例 5-24 定义一个 名为 metro_data的namedtuple,使用 attrgetter 处理它
>>> from collections import namedtuple
>>> 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]
>>>
>>> metro_areas[0]
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
>>>
>>> metro_areas[0].coord
LatLong(lat=35.689722, long=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)
>>>
说明:
#1 使用 namedtuple 定义 LatLong。
#2 再定义 Metropolis。
#3 使用 Metropolis 实例构建 metro_areas 列表;注意,我们使用嵌套的元组拆包提取 (lat, long),然后使用它们构建 LatLong,作为Metropolis 的 coord 属性。
#4 深入 metro_areas[0],获取它的纬度。
#5 定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性。
#6 再次使用 attrgetter,按照纬度排序城市列表。
#7 使用标号5中定义的 attrgetter,只显示城市名和纬度。
以下是 operator 模块中定义的部分函数(省略了以 _ 开头的名称)
>>> import 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']
>>>
说明:以 i 开头、后面是另一个运算符的那些名称(如 iadd、iand 等),对应的是增量赋值运算符(如+=、&= 等)。如果第一个参数是可变的,那么这些运算符函数会就地修改它;否则,作用与不带 i 的函数一样,直接返回运算结果。
(4)methodcaller
methodcaller的作用与 attrgetter 和 itemgetter 类似,它会自行创建函数。methodcaller 创建的函数会在对象上调用参数指定的方法。
示例 5-25 methodcaller 使用示例
#第一个测试只是为了展示 methodcaller 的用法,如果想把 str.upper 作为函数使用,
#只需在 str 类上调用,并传入一个字符串参数,str.upper(s)
>>> from operator import methodcaller
>>> s = 'The time has come'
>>> upcase = methodcaller('upper')
>>> upcase(s)
'THE TIME HAS COME'
>>>
#第二个测试表明,methodcaller 还可以冻结某些参数,
#也就是部分应用(partial application),这与 functools.partial 函数的作用类似。
>>> hiphenate = methodcaller('replace', ' ', '-')
>>> hiphenate(s)
'The-time-has-come'
>>>
5.10.2 使用functools.partial冻结参数
functools 模块提的高阶函数中,最为人熟知的或许是reduce。余下的函数中,最有用的是
partial 及其变体,partialmethod。
functools.partial 这个高阶函数用于部分应用一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的API,这样参数更少。
functools.partialmethod 函数(Python 3.4 新增)的作用与partial 一样,不过是用于处理方法的。
示例 5-26 使用 partial 把一个两参数函数改编成需要单参数的可调用对象
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3) #使用 mul 创建 triple 函数,把第一个定位参数定为 3
>>> triple(6)
18
>>> list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]
>>>
示例 5-28 把 partial 应用到示例 5-10 中的 tag 函数上,冻结一个定位参数和一个关键字参数。
>>> from functools import partial
>>> picture = partial(tag, 'img', cls='pic-frame') #1
>>> picture(src='Spongebob.jpg')
'<img class="pic-frame" src="Spongebob.jpg" />'
>>>
>>> picture
functools.partial(<function tag at 0x7f7fcb02a5e0>, 'img', cls='pic-frame') #2
>>> picture.func #3
<function tag at 0x7f7fcb02a5e0>
>>> tag
<function tag at 0x7f7fcb02a5e0>
>>>
>>>
>>> picture.args #3
('img',)
>>> picture.keywords #3
{'cls': 'pic-frame'}
>>>
说明:
#1 使用 tag 创建 picture 函数,把第一个定位参数固定为 'img',把cls 关键字参数固定为 'pic-frame'。
#2 partial() 返回一个 functools.partial 对象。
#3 functools.partial 对象提供了访问原函数和固定参数的属性。