[Python] 一等函数

《流畅的Python》卢西亚诺·拉马略 第5章  一等函数 读书笔记

目录

5.1 把函数视作对象

5.2 高阶函数

5.3 匿名函数

5.4 可调用对象

5.5 用户定义的可调用类型

5.6 函数内省

5.7 从定位参数到仅限关键字参数

5.10 支持函数式编程的包

5.10.1 operator模块

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 对象提供了访问原函数和固定参数的属性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值