本文主要是 Fluent Python 第 5 章的学习笔记。这部分主要是介绍了一等对象的条件、一等函数的表现、高阶函数、函数式编程、函数注解等。

《Fluent Python》学习笔记:第 5 章 一等函数

  • 5.1 把函数视为对象
  • 5.2 高阶函数
  • 5.3 匿名函数
  • 5.4 七大可调用对象
  • 5.5 用户定义的可调用类型
  • 5.6 函数内省
  • 5.7 从位置参数到仅限关键字参数
  • 5.8 获取关于参数的信息
  • 5.9 函数注解
  • 5.10 支持函数式编程的包
  • 巨人的肩膀


在 Python 中函数(function)是一等对象(first-class objects)。编程语言理论学家把一等对象定义为满足下述条件的程序实体:

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

在 Python 中,整数、字符串和字典等都是一等对象。Python 中所有函数都是一等对象。

5.1 把函数视为对象

以下例子说明,Python 函数是对象,我么创建了一个函数,然后调用它,读取它的 __doc__ 属性,并且确定函数对象本身是 function 类的实例。

def factorial(n):
    '''return n!'''
    return 1 if n < 2 else n * factorial(n-1)

print(factorial(42))
print(factorial.__doc__)  # __doc__ 用于生成对象的帮助文本。
print(type(factorial))  # factorial 是 function 类的一个实例
1405006117752879898543142606244511569936384000000000
return n!
<class 'function'>

Python 中函数符合一等对象的定义,看下面这个例子,把 factorial 函数赋值给变量 fact,然后通过变量名调用。还可以把它作为参数传递给 map 函数,map 函数返回一个可迭代对象,里面的元素是把第一个参数(一个函数)应用到第二个参数(一个可迭代对象)中各个元素上得到的结果。

fact = factorial
print(fact)
print(fact(5))
print(map(factorial, range(11)))
<function factorial at 0x0000020FF3E2EA68>
120
<map object at 0x0000020FF32B9DC8>

有了一等函数,那么就可以进行函数式风格编程(programming in a functional style)。

5.2 高阶函数

高阶函数(higher-order function):接受函数为参数,或者把函数作为结果返回的函数就是高阶函数。如:map、sorted 等内置函数。

函数式编程范式中,最为人熟知的是 map、filter、reduce 和 apply,其中 apply 在 Python 3 中已经移除,map、filter 和 reduce 在大多数场景也有很好地替代品。

map 和 filter 由于列表推导式和生成器表达式的出现变得没有那么重要了,通常列表推导式和生成器表达式更易读,也更推荐使用。

print(list(map(fact, range(6))))  # 构建0!到5!的一个阶乘列表
print([fact(n) for n in range(6)])  # 用列表推导式执行相同操作
print(list(map(fact, filter(lambda n: n % 2, range(6)))))  # 使用map和filter计算直到5!的奇数阶乘列表
print([fact(n) for n in range(6) if n % 2])  # 用列表推导式做同样的事,换掉map和filter,并避免了lambda表达式
[1, 1, 2, 6, 24, 120]
[1, 1, 2, 6, 24, 120]
[1, 6, 120]
[1, 6, 120]

在 Python 3 中 map 和 filter 返回的是生成器,因此它们的直接替代品是生成器表达式。(在 Python 2 中它们返回列表,最接近的替代品是列表推导)
reduce 函数在 Python 2 中是内置函数,在 Python 3 中被放入了 functools 模块,这个函数最常主要用于求和。不过最好使用内置函数 sum。sum 在可读性和性能上都有改善。

from functools import reduce
from operator import add

print(reduce(add, range(100)))
print(sum(range(100)))
4950
4950

sum 和 reduce 的通用思想是把某个操作符连续应用到序列的元素上,累计之前的结果,把一系列值规约成一个值
all 和 any 也是内置的归约函数。
他们具体用法如下:

print(help(all))
print('-' * 50)
print(help(any))
Help on built-in function all in module builtins:

all(iterable, /)
    Return True if bool(x) is True for all values x in the iterable.

    If the iterable is empty, return True.

None
--------------------------------------------------
Help on built-in function any in module builtins:

any(iterable, /)
    Return True if bool(x) is True for any x in the iterable.

    If the iterable is empty, return False.

None

5.3 匿名函数

Python 中匿名函数是通过关键字 lambda 创建。由于 Python 的简单句法限制了 lambda 函数的定义提只能使用纯表达式。换而言之,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等 Python 语句。

匿名函数最适合使用在参数列表中。它除了作为参数传给高阶函数之外,Python 很少使用匿名函数。匿名函数可读性差,且不好写。看下面这个例子。

fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
print(sorted(fruits, key=lambda word: word[::-1]))  # 使用lambda表达式反转拼写,然后依此给单词列表排序
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

5.4 七大可调用对象

  1. 用户自定义的函数:用 def 语句或 lambda 表达式创建。
  2. 内置函数:用 C 语言实现的函数,如 len 或 time.strftime。
  3. 内置方法:使用 C 语言实现的方法,如 dict.get。
  4. 方法:在类的定义体中定义的函数。
  5. 类:调用类时会运行类的 __new__ 方法创建一个实例,然后运行 __init__ 方法初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。
  6. 类的实例:如果类定义了 __call__ 方法,那么它的实例可以当做函数调用。
  7. 生成器函数:使用 yield 关键字的函数或方法,调用生成器函数返回的是生成器对象。

Python 中可以用内置函数 callable() 判断对象能否调用。

5.5 用户定义的可调用类型

任何 Python 对象都可以表现得像函数,只要实现实例方法 __call__

# bingocall.py:调用 BingoCage实例,从打乱的列表中取出一个元素
import random

class BingoCage(object):

    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexErrror:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):
        return self.pick()


bingo = BingoCage(range(3))
print(bingo.pick())
print(bingo())
print(callable(bingo))
0
2
True

5.6 函数内省

可以用 dir()函数查看函数对象的属性。有些属性时函数专有和一般对象没有的属性。如下:

# 列出常规对象没有而函数专有的属性
class C(object):
    pass


def func():
    pass

obj = C()
print(sorted(set(dir(func)) - set(dir(obj))))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

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

这部分具体参看之前总结的博客 《一文了解 Python 函数》

5.8 获取关于参数的信息

def clip(text, max_len=80):
    """在max_len前面或者后面的第一个空格处截断文本
    """
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
        if space_after >= 0:
            end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

print(clip.__defaults__)
print(clip.__code__)
print(clip.__code__.co_varnames)
print(clip.__code__.co_argcount)
(80,)
<code object clip at 0x0000020FF3AEC660, file "<ipython-input-21-164c7f53ec1b>", line 1>
('text', 'max_len', 'end', 'space_before', 'space_after')
2

使用 inspect 模块更好的查看函数的信息

from inspect import signature

sig =signature(clip)
print(sig)
print(str(sig))
for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)
(text, max_len=80)
(text, max_len=80)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

5.9 函数注解

Python 3 中可以为函数声明中的参数和返回值附加元数据。函数声明中的各个参数可以在 : 之后增加注解表达式(annotation expression),如果参数有表达式,注解放在参数名和 = 之间,如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 ->和一个表达式。表达式可以是任何类型,注解中最常用的类型是类(如 str 或 int)和字符串(如 ‘int > 0’)。

注解不会做任何处理,只是存储在函数的 __annotations__ 属性(一个字典)中。Python 对注解所做的唯一事是把他们存储在函数的 __annotations__属性里。Python 不做检查、不做强制、不做验证,什么操作都不做。也就是说注解对于 Python 解释器没有任何意义。不过,注解可以供 IDE、框架和装饰器等工具使用,为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。

# 有注解的 clip 函数
def clip(text:str, max_len:'int > 0'=80) -> str:
    """在max_len前面或者后面的第一个空格处截断文本
    """
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
        if space_after >= 0:
            end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

print(clip.__annotations__)
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

5.10 支持函数式编程的包

operator 和 functools 等包的支持使得 Python 函数式编程风格也可以信手拈来。

operator 模块为多个算术运算提供了对应的函数,从而避免了编写 lambda a, b: a*b 这种匿名函数。其中的 itemgetter 和 attrgetter 函数会自行构建函数,从序列中取出元素或者读取对象属性。如:itemgetter(1) 的作用与 lambda fields: fields[1] 一样。

functools 模块提供了一系列高阶函数,如 reduce。此外还有一个非常有用的函数 partial 及其变体,partialmethod。

简单的说 functools.partial 是一个高阶函数,它的作用是把一个函数的一些参数固定,并返回一个新的函数对象。这样做的好处是,参数更少,调用更加方便。如下面这个例子:

# 使用 partial 把两个参数函数改编成需要单参数的函数并返回,原函数不改变。
from operator import mul
from functools import partial

triple = partial(mul, 3)  # 固定一个参数为3,并返回新的改编后的函数
print(triple(7))  # 即计算 3*7
print(list(map(triple, range(1, 10))))
print(mul(4, 5))  # 原函数不受影响
21
[3, 6, 9, 12, 15, 18, 21, 24, 27]
20

巨人的肩膀

  1. 《Fluent Python》
  2. 《流畅的 Python》