一等函数
在 Python 中,函数是一等对象。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
在 Python 中,整数、字符串和字典都是一等对象——没什么特别的。如果在 Python 之前,你使用的语言并未把函数当作一等公民,那么本节以及第三部分余下的内容将重点讨论把函数作为对象的影响和实际应用。1
把函数视作对象
下面的控制台会话表明,Python 函数是对象。这里我们创建了一个函数,然后调用它,读取它的 __doc__
属性,并且确定函数对象本身是 function 类的实例。
def factorial(n):
"""returns n!"""
return 1 if n < 2 else n * factorial(n - 1)
print(factorial(42)) # 1405006117752879898543142606244511569936384000000000
print(factorial.__doc__) # 'returns n!'
# __doc__ 属性用于生成对象的帮助文本。
print(type(factorial)) # <class 'function'>
下面代码展示了函数对象的“一等”本性。我们可以把 factorial
函数赋值给变量 fact
,然后通过变量名调用。我们还能把它作为参数传给map
函数。map
函数返回一个可迭代对象,里面的元素是把第一个参数(一个函数)应用到第二个参数(一个可迭代对象,这里是range(11)
)中各个元素上得到的结果。
fact = factorial
print(fact) # <function factorial at 0x...>
fact(5) # 120
print(map(factorial, range(11))) # <map object at 0x...>
print(list(map(fact, range(11)))) # [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数。
高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)map
函数就是一例,如上述代码所示。此外,内置函数 sorted
也是:可选的 key
参数用于提供一个函数,它会应用到各个元素上进行排序,
根据单词长度给一个列表排序
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=len)) # ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
任何单参数函数都能作为key
参数的值。例如,为了创建押韵词典,可以把各个单词反过来拼写,然后排序。
def reverse(word):
return word[::-1]
print(reverse('testing')) # 'gnitset'
print(sorted(fruits, key=reverse)) # ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
在函数式编程范式中,最为人熟知的高阶函数有map
、filter
、reduce
和 apply
。apply
函数在 Python 2.3 中标记为过时,在 Python 3 中移除了,因为不再需要它了。如果想使用不定量的参数调用函数,可以编写 fn(*args, **keywords)
,不用再编写apply(fn, args, kwargs)
。
map
、filter
和 reduce
这三个高阶函数还能见到,不过多数使用场景下都有更好的替代品。
map、filter和reduce的现代替代品
函数式语言通常会提供 map
、filter
和 reduce
三个高阶函数(有时使用不同的名称)。在 Python 3 中,map
和 filter
还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了。列表推导或生成器表达式具有 map
和 filter
两个函数的功能,而且更易于阅读。
计算阶乘列表:map
和 filter
与列表推导比较:
def factorial(n):
"""returns n!"""
return 1 if n < 2 else n * factorial(n - 1)
fact = factorial
print(list(map(fact, range(6)))) # [1, 1, 2, 6, 24, 120]
print([fact(n) for n in range(6)]) # [1, 1, 2, 6, 24, 120]
print(list(map(factorial, filter(lambda n: n % 2, range(6))))) # [1, 6, 120]
print([factorial(n) for n in range(6) if n % 2]) # [1, 6, 120]
在Python 3中,map
和 filter
返回生成器(一种迭代器),因此现在它们的直接替代品是生成器表达式(在 Python 2 中,这两个函数返回列表,因此最接近的替代品是列表推导)。
在 Python 2 中,reduce
是内置函数,但是在 Python 3 中放到functools
模块里了。这个函数最常用于求和,自 2003 年发布的Python 2.3 开始,最好使用内置的 sum
函数。在可读性和性能方面,这是一项重大改善。
使用 reduce
和 sum
计算 0~99 之和:
from functools import reduce
from operator import add
print(reduce(add, range(100))) # 4950
print(sum(range(100))) # 4950
all
和 any
也是内置的归约函数:
all(iterable)
:如果iterable
的每个元素都是真值,返回True
;all([])
返回True
。any(iterable)
:只要iterable
中有元素是真值,就返回True
;any([])
返回False
。
匿名函数2
lambda
关键字在Python表达式内创建匿名函数。
然而,Python 简单的句法限制了 lambda
函数的定义体只能使用纯表达式。换句话说,lambda
函数的定义体中不能赋值,也不能使用 while
和 try
等 Python 语句。在参数列表中最适合使用匿名函数。
使用 lambda
表达式反转拼写,然后依此给单词列表排序.
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=lambda word: word[::-1])) # ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
除了作为参数传给高阶函数之外,Python 很少使用匿名函数。由于句法上的限制,非平凡的 lambda
表达式要么难以阅读,要么无法写出。
如果使用 lambda
表达式导致一段代码难以理解,Fredrik Lundh建议像下面这样重构。3
- 编写注释,说明
lambda
表达式的作用。 - 研究一会儿注释,并找出一个名称来概括注释。
- 把
lambda
表达式转换成def
语句,使用那个名称来定义函数。 - 删除注释。
lambda
句法只是语法糖:与 def
语句一样,lambda
表达式会创建函数对象。这是 Python 中几种可调用对象的一种。下一节会说明所有可调用对象。
可调用对象
除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable()
函数。
Python数据模型文档列出了 7 种可调用对象。
- 用户定义的函数:使用
def
语句或lambda
表达式创建。 - 内置函数:使用 C 语言(CPython)实现的函数,如
len
或time.strftime
。 - 内置方法:使用 C 语言实现的方法,如
dict.get
。 - 方法:在类的定义体中定义的函数。
- 类:调用类时会运行类的
__new__
方法创建一个实例,然后运行__init__
方法,初始化实例,最后把实例返回给调用方。因为 Python没有new
运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖__new__
方法的话,也可能出现其他行为。) - 类的实例:如果类定义了
__call__
方法,那么它的实例可以作为函数调用。 - 生成器函数:使用
yield
关键字的函数或方法。调用生成器函数返回的是生成器对象。
Python 中有各种各样可调用的类型,因此判断对象能否调用,最安全的方法是使用内置的 callable()
函数:
print(abs, str, 13) # (<built-in function abs>, <class 'str'>, 13)
print([callable(obj) for obj in (abs, str, 13)]) # [True, True, False]
用户定义的可调用类型
不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函数。为此,只需实现实例方法 __call__
。
调用 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()
注意,bingo 实例可以作为函数调用,而且内置的 callable(…) 函数判定它是可调用的对象:
bingo = BingoCage(range(3))
print(bingo.pick()) # 1
print(bingo()) # 0
print(callable(bingo)) # True
实现 __call__
方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间“记住”某些事 [例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ]。
创建保有内部状态的函数,还有一种截然不同的方式——使用闭包。下下章会讨论。
python进阶书目串烧(一)—— 特殊方法、序列数组、列表推导、生成器表达
python进阶书目串烧(二)—— 元组拆包、具名元组、元组对比列表、切片
python进阶书目串烧(三)—— 序列、排序、列表对比数组
python进阶书目串烧(四)—— 内存视图、NumPy、列表对比双向队列
python进阶书目串烧(五)—— 泛映射类型、字典推导、映射的弹性键查询
python进阶书目串烧(六)—— 字典变种、不可变映射类型、集合推导
python进阶书目串烧(七)—— 字典原理、字典与集合特征对比
python进阶书目串烧(八)—— 内容透视:字符、字节、编解码
python进阶书目串烧(九)—— 内容透视:高阶函数、匿名函数、可调用对象
人们经常将“把函数视作一等对象”简称为“一等函数”。这样说并不完美,似乎表明这是函数中的特殊群体。在 Python 中,所有函数都是一等对象。 ↩︎
B2中涉及到lambda使用的位置:7.6 定义匿名或内联函数、7.7 匿名函数捕获变量值 ↩︎
这几步摘自“Functional Programming HOWTO”,这是一篇必读文章。 ↩︎