读《流畅的Python》:一等函数
前言
一等对象(first-class objects)是指拥有如下特性的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
Python中的函数拥有这几个特性,所以被称作一等函数(functions as first-class objects,简称first-class functions)。
把函数视作对象
1 | |
高阶函数
接受函数为参数,或把函数作为结果返回的函数是高阶函数,比如上文提到的print()和fooo()。再比如接收函数作为key参数的sorted():
1 | |
函数式编程语言一般会提供map、filter、reduce三个高阶函数。Python也提供这三个函数,但列表推导(list comprehensions)和生成器表达式(generator expressions)在完成和map、filter相同的功能时代码可读性更好:
1 | |
如果是执行求和操作,也可以用内置的sum函数替代reduce,性能和可读性更好:
1 | |
和sum和reduce的思路类似,all和any也是两个内置的归约函数:
all(iterable):如果每个元素都为真,返回True。all([])返回True。any(iterable):如任意一个元素为真,返回True。any([])返回False。
匿名函数
用lambda关键字创造的函数叫做匿名函数。匿名函数只能使用纯表达式,函数内不能赋值,也不能使用while、try等语句。匿名函数一般只作为参数传递给高阶函数,就像上一节的示例那样。
从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 | |
函数内省
内省指程序在运行时检查对象类型的一种能力,在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 | |
函数中的参数可以在:之后增加注解表达式。如参数有默认值,则表达式放在参数和=号之间。函数末尾的)和:之间也可以放入表达式,用于注解返回值。注解不会做任何处理,只是储存在函数的__annotations__属性里:
1 | |
注解只是元数据,可以供IDE、框架、装饰器、静态代码分析工具等使用。标准库中只有上一节提到的inspect.signature会用到注解,如:
1 | |
用注解实现参数校验
Python大牛Raymond Hettinger在一个StackOverflow问答里展示了一种方便的、基于注解的参数校验机制,摘录如下:
1 | |
1 | |
其基本思想是用Callable对象充当函数参数的注解,然后在函数被调用时调用Callable对象来校验参数。换句话说,上面的函数f可以改写成如下形式:
1 | |
由此可见前文参数校验机制的精妙。
支持函数式编程的模块
在operator和functools等模块的支持下,Python也可以很方便地实现函数式编程风格。
operator模块
operator模块为许多算数运算符提供了对应的函数,如add(对应a + b)、mul(对应a * b)等等。比如计算1~n的和与n的阶乘:
1 | |
operator模块还提供了itemgetter和attrgetter两个函数,可以从序列中提取元素或读取对象的属性。比如:
1 | |
最后介绍methodcaller函数,这个函数创建的函数会调用对象上的指定方法,如:
1 | |
用functools.partial冻结参数
除了前文已经多次提到的reduce,functools模块还提供了许多有用的函数,比如可以冻结函数参数的partial。上一节提到的append_lemon其实就起到了类似partial的作用,但partial的功能更强大。比如前文提到的自定义排序,可以用更通用的形式重写:
1 | |
1 | |
在需要用相同(或类似)的参数反复调用某个函数时,使用functools.partial冻结参数可以有效降低工作量和bug出现的几率。functools还提供了wraps、lru_cache等比较实用的装饰器,这方面的内容可以参考笔者先前的文章。