读《流畅的Python》:一等函数

前言

一等对象(first-class objects)是指拥有如下特性的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

Python中的函数拥有这几个特性,所以被称作一等函数functions as first-class objects,简称first-class functions)。

把函数视作对象

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def foo(x):  # 在运行时创建
... """return x * x"""
... return x * x
...
>>> bar = foo # 赋值给变量
>>> bar.__doc__
'return x * x'
>>> print(bar) # 作为参数传给函数
<function foo at ...>
>>> def fooo(): return foo
...
>>> fooo()(10) # 作为函数的返回结果
100

高阶函数

接受函数为参数,或把函数作为结果返回的函数是高阶函数,比如上文提到的print()fooo()。再比如接收函数作为key参数的sorted()

1
2
>>> sorted([-2, -1, 0, 1, 2], key=abs)
[0, -1, 1, -2, 2]

函数式编程语言一般会提供mapfilterreduce三个高阶函数。Python也提供这三个函数,但列表推导(list comprehensions)和生成器表达式(generator expressions)在完成和mapfilter相同的功能时代码可读性更好:

1
2
3
4
5
6
7
8
9
>>> list(map(foo, range(6)))
[0, 1, 4, 9, 16, 25]
>>> [foo(x) for x in range(6)]
[0, 1, 4, 9, 16, 25]
>>>
>>> list(map(foo, filter(lambda x: x > 2, range(6)))) # 用lambda关键字创造了一个匿名函数
[9, 16, 25]
>>> [foo(x) for x in range(6) if x > 2]
[9, 16, 25]

如果是执行求和操作,也可以用内置的sum函数替代reduce,性能和可读性更好:

1
2
3
4
5
6
>>> from functools import reduce  # Python3起,reduce不再是内置函数
>>> from operator import add # add 相当于 lambda a, b: a + b
>>> reduce(add, range(100))
4950
>>> sum(range(100))
4950

sumreduce的思路类似,allany也是两个内置的归约函数:

  • all(iterable):如果每个元素都为真,返回Trueall([])返回True
  • any(iterable):如任意一个元素为真,返回Trueany([])返回False

匿名函数

lambda关键字创造的函数叫做匿名函数。匿名函数只能使用纯表达式,函数内不能赋值,也不能使用whiletry等语句。匿名函数一般只作为参数传递给高阶函数,就像上一节的示例那样。

Pythonic的角度来说,匿名函数最好不要超过一行,也不推荐将匿名函数赋值给其它变量。

可调用对象

能被调用运算符(即())应用的对象被称为可调用对象,这点可以通过内置的callable函数来判断。Python数据模型文档列出了7种可调用对象:

  • 用户定义的函数(User-defined functions),使用def语句和lambda表达式创建的函数。
  • 方法(Instance methods),在类的定义体中定义的函数。
  • 生成器函数(Generator functions),使用yield关键字的函数或方法。调用生成器函数会返回生成器对象。
  • 内置函数(Built-in functions)、内置方法(Built-in methods),用C语言实现的函数/方法,如len()alist.append()
  • 类(Classes),调用类时会运行类的__new__方法创建实例,然后运行__init__方法初始化实例。
  • 类的实例(Class Instances),定义了__call__方法的类的实例。

当然,《流畅的Python》基于Python3.4,后续的Python版本还引入了其它种类的可调用对象,如协程函数(Coroutine functions)和异步生成器函数(Asynchronous generator functions),详见前文的文档链接。

1
2
3
4
>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs, str, 13)]
[True, True, False]

函数内省

内省指程序在运行时检查对象类型的一种能力,在Python中,函数提供许多属性来实现内省。函数特有的属性主要有如下几种:

名称 类型 说明
__annotations__ dict 参数和返回值的注解
__call__ method-wrapper 实现()运算符
__closure__ tuple 函数闭包,即自由变量的绑定
__code__ code 编译成字节码的函数元数据和函数定义体
__defaults__ tuple 形参的默认值
__get__ method-wraper 实现只读描述符协议
__globals__ dict 函数所在模块的全局变量
__kwdefaults__ dict 仅限关键字形参的默认值
__name__ str 函数名称
__qualname__ str 函数的限定名称,如Random.choice

函数参数、获取关于参数的信息

详见上一篇文章

函数注解

Python3提供的新语法函数注解可以为函数声明的参数和返回值附加元数据,如:

1
2
def foo(num_a: 'int > 0', num_b: str = '123') -> float:
return (num_a + int(num_b)) / 2

函数中的参数可以在:之后增加注解表达式。如参数有默认值,则表达式放在参数和=号之间。函数末尾的):之间也可以放入表达式,用于注解返回值。注解不会做任何处理,只是储存在函数的__annotations__属性里:

1
2
>>> foo.__annotations__
{'num_a': 'int > 0', 'num_b': <class 'str'>, 'return': <class 'float'>}

注解只是元数据,可以供IDE、框架、装饰器、静态代码分析工具等使用。标准库中只有上一节提到的inspect.signature会用到注解,如:

1
2
3
4
5
6
7
8
9
10
>>> from inspect import signature
>>> sig = signature(foo)
>>> sig.return_annotation
<class 'float'>
>>> for param in sig.parameters.values():
... anno = str(param.annotation)
... print(f'{anno:<13} : {param.name} = {param.default}')
...
int > 0 : num_a = <class 'inspect._empty'>
<class 'str'> : num_b = 123

用注解实现参数校验

Python大牛Raymond Hettinger在一个StackOverflow问答里展示了一种方便的、基于注解的参数校验机制,摘录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def validate(func, locals):
for var, test in func.__annotations__.items():
value = locals[var]
msg = 'Var: {0}\tValue: {1}\tTest: {2.__name__}'.format(var, value, test)
assert test(value), msg

def is_int(x):
return isinstance(x, int)

def between(lo, hi):
def _between(x):
return lo <= x <= hi
return _between

def f(x: between(3, 10), y: is_int):
validate(f, locals())
print(x, y)
1
2
3
4
5
6
>>> f(3, 1)
3 1
>>> f(0, 31.1)
Traceback (most recent call last):
...
AssertionError: Var: x Value: 0 Test: _between

其基本思想是用Callable对象充当函数参数的注解,然后在函数被调用时调用Callable对象来校验参数。换句话说,上面的函数f可以改写成如下形式:

1
2
3
4
def f(x, y):
assert 3 <= x <= 10, f'Var: x Value: {x} Test: _between'
assert isinstance(y, int), f'Var: y Value: {y} Test: is_int'
print(x, y)

由此可见前文参数校验机制的精妙。

支持函数式编程的模块

operatorfunctools等模块的支持下,Python也可以很方便地实现函数式编程风格。

operator模块

operator模块为许多算数运算符提供了对应的函数,如add(对应a + b)、mul(对应a * b)等等。比如计算1~n的和与n的阶乘:

1
2
3
4
5
6
7
>>> from functools import reduce
>>> from operator import add, mul
>>> n = 10
>>> reduce(add, range(1, n+1)) # 1 + 2 + ... + 10
55
>>> reduce(mul, range(1, n+1)) # 1 * 2 * ... * 10
3628800

operator模块还提供了itemgetterattrgetter两个函数,可以从序列中提取元素或读取对象的属性。比如:

1
2
3
4
5
6
7
>>> records = [(1, 'orange'), (2, 'apple'), (3, 'juice')]
>>> from operator import itemgetter, attrgetter
>>> sorted(records, key=itemgetter(1)) # itemgetter(1) 等效于 lambda x: x[1]
[(2, 'apple'), (3, 'juice'), (1, 'orange')]
>>>
>>> attrgetter('append')(records)
<built-in method append of list object at ...>

最后介绍methodcaller函数,这个函数创建的函数会调用对象上的指定方法,如:

1
2
3
4
5
6
7
>>> from operator import methodcaller
>>> records
[(1, 'orange'), (2, 'apple'), (3, 'juice')]
>>> append_lemon = methodcaller('append', (4, 'lemon'))
>>> append_lemon(records) # 等效于 records.append((4, 'lemon'))
>>> records
[(1, 'orange'), (2, 'apple'), (3, 'juice'), (4, 'lemon')]

用functools.partial冻结参数

除了前文已经多次提到的reducefunctools模块还提供了许多有用的函数,比如可以冻结函数参数的partial。上一节提到的append_lemon其实就起到了类似partial的作用,但partial的功能更强大。比如前文提到的自定义排序,可以用更通用的形式重写:

1
2
3
4
>>> records = [(1, 'orange'), (2, 'apple'), (3, 'juice')]
>>> from operator import itemgetter, attrgetter
>>> sorted(records, key=itemgetter(1)) # itemgetter(1) 等效于 lambda x: x[1]
[(2, 'apple'), (3, 'juice'), (1, 'orange')]
1
2
3
4
>>> from functools import partial
>>> sort_by_second = partial(sorted, key=itemgetter(1))
>>> sort_by_second(records)
[(2, 'apple'), (3, 'juice'), (1, 'orange')]

在需要用相同(或类似)的参数反复调用某个函数时,使用functools.partial冻结参数可以有效降低工作量和bug出现的几率。functools还提供了wrapslru_cache等比较实用的装饰器,这方面的内容可以参考笔者先前的文章


读《流畅的Python》:一等函数
https://www.yooo.ltd/2020/07/05/fluent-python-chap-5/
作者
OrangeWolf
发布于
2020年7月5日
许可协议